Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

// A ServiceLocator is a scoped locator: its keys are local (e.g. tag index
// attributes from #[AutowireLocator]), not global container service ids, so the
// ServiceMap has no knowledge of them and cannot tell whether they are registered.
$isServiceLocatorType = (new ObjectType('Symfony\Component\DependencyInjection\ServiceLocator'))->isSuperTypeOf($argType);
if ($isServiceLocatorType->yes()) {
return [];
}

$isControllerType = (new ObjectType('Symfony\Bundle\FrameworkBundle\Controller\Controller'))->isSuperTypeOf($argType);
$isAbstractControllerType = (new ObjectType('Symfony\Bundle\FrameworkBundle\Controller\AbstractController'))->isSuperTypeOf($argType);
$isContainerType = (new ObjectType('Symfony\Component\DependencyInjection\ContainerInterface'))->isSuperTypeOf($argType);
Expand Down
8 changes: 8 additions & 0 deletions src/Type/Symfony/ServiceDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ private function getHasTypeFromMethodCall(
return null;
}

// A ServiceLocator is a scoped locator: its keys are local (e.g. tag index
// attributes from #[AutowireLocator]), not global container service ids. The
// ServiceMap only knows the global container, so it cannot decide has() here;
// returning a constant bool would wrongly make guard branches unreachable.
if ((new ObjectType('Symfony\Component\DependencyInjection\ServiceLocator'))->isSuperTypeOf($scope->getType($methodCall->var))->yes()) {
return null;
}

$serviceId = $this->serviceMap::getServiceIdFromNode($methodCall->getArgs()[0]->value, $scope);
if ($serviceId !== null) {
$service = $this->serviceMap->getService($serviceId);
Expand Down
14 changes: 14 additions & 0 deletions tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ public function testGetPrivateServiceInLegacyServiceSubscriber(): void
);
}

public function testServiceLocatorKeyIsNotReportedAsUnknownService(): void
{
if (!class_exists('Symfony\Component\DependencyInjection\ServiceLocator')) {
self::markTestSkipped('The test needs Symfony\Component\DependencyInjection\ServiceLocator class.');
}

$this->analyse(
[
__DIR__ . '/ExampleServiceLocatorConsumer.php',
],
[],
);
}

public static function getAdditionalConfigFiles(): array
{
return [
Expand Down
24 changes: 24 additions & 0 deletions tests/Rules/Symfony/ExampleServiceLocatorConsumer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Symfony;

use Symfony\Component\DependencyInjection\ServiceLocator;

final class ExampleServiceLocatorConsumer
{

/**
* @param ServiceLocator<object> $locator
*/
public function __construct(private readonly ServiceLocator $locator)
{
}

public function run(): void
{
// "unknown" is not a global container service id, but it is a valid locator
// index key (e.g. from #[AutowireLocator]), so no serviceNotFound must be reported.
$this->locator->get('unknown');
}

}
2 changes: 2 additions & 0 deletions tests/Type/Symfony/ExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public function dataFileAsserts(): iterable

yield from $this->gatherAssertTypes(__DIR__ . '/data/tree_builder.php');

yield from $this->gatherAssertTypes(__DIR__ . '/data/service_locator.php');

yield from $this->gatherAssertTypes(__DIR__ . '/data/ExampleBaseCommand.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/ExampleOptionCommand.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/ExampleOptionLazyCommand.php');
Expand Down
37 changes: 37 additions & 0 deletions tests/Type/Symfony/data/service_locator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types = 1);

namespace SymfonyServiceLocator;

use Symfony\Component\DependencyInjection\ServiceLocator;
use function PHPStan\Testing\assertType;

interface HandlerInterface
{

public function handle(): void;

}

class Consumer
{

/**
* @param ServiceLocator<HandlerInterface> $locator
*/
public function __construct(
private readonly ServiceLocator $locator,
)
{
}

public function run(string $key): void
{
// has() on a ServiceLocator must NOT collapse to a constant bool: a locator key
// is a local index, not a global service id, so the ServiceMap cannot decide it.
// Otherwise a `if (!$locator->has('x')) { return; }` guard would be seen as
// "always true" and everything after it as unreachable.
assertType('bool', $this->locator->has($key));
assertType('bool', $this->locator->has('some_index'));
}

}
Loading