diff --git a/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php b/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php index 996d3b77..e0ffc1cb 100644 --- a/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php +++ b/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php @@ -7,10 +7,13 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Symfony\AutowireLocatorServiceMapFactory; +use PHPStan\Symfony\DefaultServiceMap; use PHPStan\Symfony\ServiceMap; use PHPStan\TrinaryLogic; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use function is_null; use function sprintf; /** @@ -66,15 +69,25 @@ public function processNode(Node $node, Scope $scope): array } $serviceId = $this->serviceMap::getServiceIdFromNode($node->getArgs()[0]->value, $scope); - if ($serviceId !== null) { - $service = $this->serviceMap->getService($serviceId); - if ($service !== null && !$service->isPublic()) { - return [ - RuleErrorBuilder::message(sprintf('Service "%s" is private.', $serviceId)) - ->identifier('symfonyContainer.privateService') - ->build(), - ]; - } + if ($serviceId === null) { + return []; + } + + $isContainerInterfaceType = $isContainerType->yes() || $isPsrContainerType->yes(); + if ( + $isContainerInterfaceType && + $this->isAutowireLocatorService($node, $scope, $serviceId) + ) { + return []; + } + + $service = $this->serviceMap->getService($serviceId); + if ($service !== null && !$service->isPublic()) { + return [ + RuleErrorBuilder::message(sprintf('Service "%s" is private.', $serviceId)) + ->identifier('symfonyContainer.privateService') + ->build(), + ]; } return []; @@ -92,4 +105,16 @@ private function isServiceSubscriber(Type $containerType, Scope $scope): Trinary return $isContainerServiceSubscriber->or($serviceSubscriberInterfaceType->isSuperTypeOf($containedClassType)); } + private function isAutowireLocatorService(Node $node, Scope $scope, string $serviceId): bool + { + $autowireLocatorServiceMapFactory = new AutowireLocatorServiceMapFactory($node, $scope); + $autowireLocatorServiceMap = $autowireLocatorServiceMapFactory->create(); + + if (!$autowireLocatorServiceMap instanceof DefaultServiceMap) { + return false; + } + + return !is_null($autowireLocatorServiceMap->getService($serviceId)); + } + } diff --git a/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php b/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php index fc7d9585..cb29dd33 100644 --- a/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php +++ b/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php @@ -6,8 +6,11 @@ use PhpParser\Node\Expr\MethodCall; use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Symfony\AutowireLocatorServiceMapFactory; +use PHPStan\Symfony\DefaultServiceMap; use PHPStan\Symfony\ServiceMap; use PHPStan\Type\ObjectType; use PHPStan\Type\Symfony\Helper; @@ -66,19 +69,56 @@ public function processNode(Node $node, Scope $scope): array } $serviceId = $this->serviceMap::getServiceIdFromNode($node->getArgs()[0]->value, $scope); - if ($serviceId !== null) { - $service = $this->serviceMap->getService($serviceId); - $serviceIdType = $scope->getType($node->getArgs()[0]->value); - if ($service === null && !$scope->getType(Helper::createMarkerNode($node->var, $serviceIdType, $this->printer))->equals($serviceIdType)) { + if ($serviceId === null) { + return []; + } + + $isContainerInterfaceType = $isContainerType->yes() || $isPsrContainerType->yes(); + if ($isContainerInterfaceType) { + $autowireLoaderResult = $this->getAutowireLocatorResult($node, $scope, $serviceId); + + if ($autowireLoaderResult !== null) { + return $autowireLoaderResult; + } + } + + $service = $this->serviceMap->getService($serviceId); + $serviceIdType = $scope->getType($node->getArgs()[0]->value); + if ($service === null && !$scope->getType(Helper::createMarkerNode($node->var, $serviceIdType, $this->printer))->equals($serviceIdType)) { + return [ + RuleErrorBuilder::message(sprintf('Service "%s" is not registered in the container.', $serviceId)) + ->identifier('symfonyContainer.serviceNotFound') + ->build(), + ]; + } + + return []; + } + + /** + * @return list|null + */ + private function getAutowireLocatorResult(Node $node, Scope $scope, string $serviceId): ?array + { + $autowireLocatorServiceMapFactory = new AutowireLocatorServiceMapFactory($node, $scope); + $autowireLocatorServiceMap = $autowireLocatorServiceMapFactory->create(); + + // Our container has a valid AutowireLocator attribute, else we would get a FakeServiceMap. + if ($autowireLocatorServiceMap instanceof DefaultServiceMap) { + $autowireLocatorService = $autowireLocatorServiceMap->getService($serviceId); + + if ($autowireLocatorService === null) { return [ - RuleErrorBuilder::message(sprintf('Service "%s" is not registered in the container.', $serviceId)) - ->identifier('symfonyContainer.serviceNotFound') + RuleErrorBuilder::message(sprintf('Service "%s" is not registered in the AutowireLocator.', $serviceId)) + ->identifier('symfonyContainer.undefinedService') ->build(), ]; } + + return []; } - return []; + return null; } } diff --git a/src/Symfony/AutowireLocatorServiceMapFactory.php b/src/Symfony/AutowireLocatorServiceMapFactory.php new file mode 100644 index 00000000..839f621a --- /dev/null +++ b/src/Symfony/AutowireLocatorServiceMapFactory.php @@ -0,0 +1,110 @@ +node = $node; + $this->scope = $scope; + } + + public function create(): ServiceMap + { + if (!class_exists('Symfony\\Component\\DependencyInjection\\Attribute\\AutowireLocator')) { + return new FakeServiceMap(); + } + + if (!$this->node instanceof MethodCall) { + return new FakeServiceMap(); + } + + $nodeParentProperty = $this->node->var; + + if (!$nodeParentProperty instanceof Node\Expr\PropertyFetch) { + return new FakeServiceMap(); + } + + $nodeParentPropertyName = $nodeParentProperty->name; + + if (!$nodeParentPropertyName instanceof Node\Identifier) { + return new FakeServiceMap(); + } + + $containerInterfacePropertyName = $nodeParentPropertyName->name; + $scopeClassReflection = $this->scope->getClassReflection(); + + if (!$scopeClassReflection instanceof ClassReflection) { + return new FakeServiceMap(); + } + + $containerInterfacePropertyReflection = $scopeClassReflection + ->getNativeProperty($containerInterfacePropertyName); + $classPropertyReflection = $containerInterfacePropertyReflection->getNativeReflection(); + $autowireLocatorAttributesReflection = $classPropertyReflection->getAttributes(AutowireLocator::class); + + if (count($autowireLocatorAttributesReflection) === 0) { + return new FakeServiceMap(); + } + + if (count($autowireLocatorAttributesReflection) > 1) { + throw new InvalidArgumentException(sprintf( + 'Only one AutowireLocator attribute is allowed on "%s::%s".', + $scopeClassReflection->getName(), + $containerInterfacePropertyName + )); + } + + $autowireLocatorAttributeReflection = $autowireLocatorAttributesReflection[0]; + /** @var AutowireLocator $autowireLocator */ + $autowireLocator = $autowireLocatorAttributeReflection->newInstance(); + $serviceLocatorArgument = $autowireLocator->value; + + if (!$serviceLocatorArgument instanceof ServiceLocatorArgument) { + return new FakeServiceMap(); + } + + /** @var Service[] $services */ + $services = []; + + /** @var TypedReference $service */ + foreach ($serviceLocatorArgument->getValues() as $id => $service) { + $class = $service->getType(); + $alias = $service->getName(); + + $services[$id] = new Service( + $id, + $class, + true, + false, + $alias + ); + } + + return new DefaultServiceMap($services); + } + +} diff --git a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php index 51513b09..c421cdd7 100644 --- a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php +++ b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php @@ -8,6 +8,7 @@ use PHPStan\Testing\RuleTestCase; use function class_exists; use function interface_exists; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -78,4 +79,37 @@ public function testGetPrivateServiceInServiceSubscriber(): void ); } + public function testGetPrivateServiceWithoutAutowireLocatorAttribute(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('The test uses PHP Attributes which are available since PHP 8.0.'); + } + + $this->analyse( + [ + __DIR__ . '/ExampleAutowireLocatorEmptyService.php', + ], + [ + [ + 'Service "private" is private.', + 22, + ], + ] + ); + } + + public function testGetPrivateServiceViaAutowireLocatorAttribute(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('The test uses PHP Attributes which are available since PHP 8.0.'); + } + + $this->analyse( + [ + __DIR__ . '/ExampleAutowireLocatorService.php', + ], + [] + ); + } + } diff --git a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php index 4bd233e9..3d185aa0 100644 --- a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php +++ b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php @@ -9,6 +9,7 @@ use PHPStan\Testing\RuleTestCase; use function class_exists; use function interface_exists; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -72,6 +73,43 @@ public function testGetPrivateServiceInLegacyServiceSubscriber(): void ); } + public function testGetPrivateServiceWithoutAutowireLocatorAttribute(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('The test uses PHP Attributes which are available since PHP 8.0.'); + } + + $this->analyse( + [ + __DIR__ . '/ExampleAutowireLocatorEmptyService.php', + ], + [ + [ + 'Service "Foo" is not registered in the AutowireLocator.', + 21, + ], + [ + 'Service "private" is not registered in the AutowireLocator.', + 22, + ], + ] + ); + } + + public function testGetPrivateServiceViaAutowireLocatorAttribute(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('The test uses PHP Attributes which are available since PHP 8.0.'); + } + + $this->analyse( + [ + __DIR__ . '/ExampleAutowireLocatorService.php', + ], + [] + ); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/Rules/Symfony/ExampleAutowireLocatorEmptyService.php b/tests/Rules/Symfony/ExampleAutowireLocatorEmptyService.php new file mode 100644 index 00000000..d59f30b2 --- /dev/null +++ b/tests/Rules/Symfony/ExampleAutowireLocatorEmptyService.php @@ -0,0 +1,25 @@ +locator = $locator; + } + + public function privateServiceInLocator(): void + { + $this->locator->get('Foo'); + $this->locator->get('private'); + } + +} diff --git a/tests/Rules/Symfony/ExampleAutowireLocatorService.php b/tests/Rules/Symfony/ExampleAutowireLocatorService.php new file mode 100644 index 00000000..e78d4859 --- /dev/null +++ b/tests/Rules/Symfony/ExampleAutowireLocatorService.php @@ -0,0 +1,27 @@ + 'Foo', + 'private' => 'Foo', + ])] + private ContainerInterface $locator + ) + { + } + + public function privateServiceInLocator(): void + { + $this->locator->get('Foo'); + $this->locator->get('private'); + } + +}