diff --git a/README.md b/README.md index b853b3fa..e93e05ba 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,32 @@ parameters: container_xml_path: var/cache/dev/App_KernelDevDebugContainer.xml ``` +The XML file should exist prior to running PHPStan. + +If you want this file to be created automatically and resolved based on `APP_ENV` you can use this: + +```yaml +parameters: + symfony: + container_xml_path: tests/container-loader.php +``` + +```php +//tests/container-loader.php + +use App\Kernel; +use Symfony\Component\Dotenv\Dotenv; + +require __DIR__ . '/../vendor/autoload.php'; + +(new Dotenv())->bootEnv(__DIR__ . '/../.env'); + +$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); +$kernel->boot(); + +return file_get_contents($kernel->getContainer()->getParameter('debug.container.dump')); +``` + ## Constant hassers Sometimes, when you are dealing with optional dependencies, the `::has()` methods can cause problems. For example, the following construct would complain that the condition is always either on or off, depending on whether you have the dependency for `service` installed: diff --git a/extension.neon b/extension.neon index 7873e2b5..4434e271 100644 --- a/extension.neon +++ b/extension.neon @@ -59,17 +59,23 @@ services: arguments: consoleApplicationLoader: %symfony.console_application_loader% + # xml container resolver + - + factory: PHPStan\Symfony\XmlContainerResolver + arguments: + containerXmlPath: %symfony.container_xml_path% + # service map symfony.serviceMapFactory: class: PHPStan\Symfony\ServiceMapFactory - factory: PHPStan\Symfony\XmlServiceMapFactory(%symfony.container_xml_path%) + factory: PHPStan\Symfony\XmlServiceMapFactory - factory: @symfony.serviceMapFactory::create() # parameter map symfony.parameterMapFactory: class: PHPStan\Symfony\ParameterMapFactory - factory: PHPStan\Symfony\XmlParameterMapFactory(%symfony.container_xml_path%) + factory: PHPStan\Symfony\XmlParameterMapFactory - factory: @symfony.parameterMapFactory::create() diff --git a/src/Symfony/XmlContainerResolver.php b/src/Symfony/XmlContainerResolver.php new file mode 100644 index 00000000..b4bfd611 --- /dev/null +++ b/src/Symfony/XmlContainerResolver.php @@ -0,0 +1,55 @@ +containerXmlPath = $containerXmlPath; + } + + public function getContainer(): ?SimpleXMLElement + { + if ($this->containerXmlPath === null) { + return null; + } + + if ($this->container !== null) { + return $this->container; + } + + if (pathinfo($this->containerXmlPath, PATHINFO_EXTENSION) === 'php') { + $fileContents = require $this->containerXmlPath; + + if (!is_string($fileContents)) { + throw new ShouldNotHappenException(); + } + } else { + $fileContents = file_get_contents($this->containerXmlPath); + if ($fileContents === false) { + throw new XmlContainerNotExistsException(sprintf('Container %s does not exist', $this->containerXmlPath)); + } + } + + $container = @simplexml_load_string($fileContents); + if ($container === false) { + throw new XmlContainerNotExistsException(sprintf('Container %s cannot be parsed', $this->containerXmlPath)); + } + + $this->container = $container; + + return $this->container; + } + +} diff --git a/src/Symfony/XmlParameterMapFactory.php b/src/Symfony/XmlParameterMapFactory.php index 6a7575f4..df196c45 100644 --- a/src/Symfony/XmlParameterMapFactory.php +++ b/src/Symfony/XmlParameterMapFactory.php @@ -7,33 +7,25 @@ final class XmlParameterMapFactory implements ParameterMapFactory { - /** @var string|null */ - private $containerXml; + /** @var XmlContainerResolver */ + private $containerResolver; - public function __construct(?string $containerXml) + public function __construct(XmlContainerResolver $containerResolver) { - $this->containerXml = $containerXml; + $this->containerResolver = $containerResolver; } public function create(): ParameterMap { - if ($this->containerXml === null) { - return new FakeParameterMap(); - } + $container = $this->containerResolver->getContainer(); - $fileContents = file_get_contents($this->containerXml); - if ($fileContents === false) { - throw new XmlContainerNotExistsException(sprintf('Container %s does not exist', $this->containerXml)); - } - - $xml = @simplexml_load_string($fileContents); - if ($xml === false) { - throw new XmlContainerNotExistsException(sprintf('Container %s cannot be parsed', $this->containerXml)); + if ($container === null) { + return new FakeParameterMap(); } /** @var \PHPStan\Symfony\Parameter[] $parameters */ $parameters = []; - foreach ($xml->parameters->parameter as $def) { + foreach ($container->parameters->parameter as $def) { /** @var \SimpleXMLElement $attrs */ $attrs = $def->attributes(); diff --git a/src/Symfony/XmlServiceMapFactory.php b/src/Symfony/XmlServiceMapFactory.php index 47aa1bc9..59bb4309 100644 --- a/src/Symfony/XmlServiceMapFactory.php +++ b/src/Symfony/XmlServiceMapFactory.php @@ -2,43 +2,33 @@ namespace PHPStan\Symfony; -use function simplexml_load_string; -use function sprintf; use function strpos; use function substr; final class XmlServiceMapFactory implements ServiceMapFactory { - /** @var string|null */ - private $containerXml; + /** @var XmlContainerResolver */ + private $containerResolver; - public function __construct(?string $containerXml) + public function __construct(XmlContainerResolver $containerResolver) { - $this->containerXml = $containerXml; + $this->containerResolver = $containerResolver; } public function create(): ServiceMap { - if ($this->containerXml === null) { - return new FakeServiceMap(); - } + $container = $this->containerResolver->getContainer(); - $fileContents = file_get_contents($this->containerXml); - if ($fileContents === false) { - throw new XmlContainerNotExistsException(sprintf('Container %s does not exist', $this->containerXml)); - } - - $xml = @simplexml_load_string($fileContents); - if ($xml === false) { - throw new XmlContainerNotExistsException(sprintf('Container %s cannot be parsed', $this->containerXml)); + if ($container === null) { + return new FakeServiceMap(); } /** @var \PHPStan\Symfony\Service[] $services */ $services = []; /** @var \PHPStan\Symfony\Service[] $aliases */ $aliases = []; - foreach ($xml->services->service as $def) { + foreach ($container->services->service as $def) { /** @var \SimpleXMLElement $attrs */ $attrs = $def->attributes(); if (!isset($attrs->id)) { diff --git a/tests/Rules/Symfony/ContainerInterfacePrivateServicePhpLoaderRuleTest.php b/tests/Rules/Symfony/ContainerInterfacePrivateServicePhpLoaderRuleTest.php new file mode 100644 index 00000000..d001a54e --- /dev/null +++ b/tests/Rules/Symfony/ContainerInterfacePrivateServicePhpLoaderRuleTest.php @@ -0,0 +1,79 @@ + + */ +final class ContainerInterfacePrivateServicePhpLoaderRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ContainerInterfacePrivateServiceRule((new XmlServiceMapFactory(new XmlContainerResolver(__DIR__ . '/container_loader.php')))->create()); + } + + public function testGetPrivateService(): void + { + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } + $this->analyse( + [ + __DIR__ . '/ExampleController.php', + ], + [ + [ + 'Service "private" is private.', + 12, + ], + ] + ); + } + + public function testGetPrivateServiceInLegacyServiceSubscriber(): void + { + if (!interface_exists('Symfony\\Component\\DependencyInjection\\ServiceSubscriberInterface')) { + self::markTestSkipped('The test needs Symfony\Component\DependencyInjection\ServiceSubscriberInterface class.'); + } + + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } + + $this->analyse( + [ + __DIR__ . '/ExampleLegacyServiceSubscriber.php', + __DIR__ . '/ExampleLegacyServiceSubscriberFromAbstractController.php', + __DIR__ . '/ExampleLegacyServiceSubscriberFromLegacyController.php', + ], + [] + ); + } + + public function testGetPrivateServiceInServiceSubscriber(): void + { + if (!interface_exists('Symfony\Contracts\Service\ServiceSubscriberInterface')) { + self::markTestSkipped('The test needs Symfony\Contracts\Service\ServiceSubscriberInterface class.'); + } + + if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\Controller')) { + self::markTestSkipped(); + } + + $this->analyse( + [ + __DIR__ . '/ExampleServiceSubscriber.php', + __DIR__ . '/ExampleServiceSubscriberFromAbstractController.php', + __DIR__ . '/ExampleServiceSubscriberFromLegacyController.php', + ], + [] + ); + } + +} diff --git a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleFakeTest.php b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleFakeTest.php index 81925291..ce5bfd77 100644 --- a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleFakeTest.php +++ b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleFakeTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Symfony; use PHPStan\Rules\Rule; +use PHPStan\Symfony\XmlContainerResolver; use PHPStan\Symfony\XmlServiceMapFactory; use PHPStan\Testing\RuleTestCase; @@ -14,7 +15,7 @@ final class ContainerInterfacePrivateServiceRuleFakeTest extends RuleTestCase protected function getRule(): Rule { - return new ContainerInterfacePrivateServiceRule((new XmlServiceMapFactory(null))->create()); + return new ContainerInterfacePrivateServiceRule((new XmlServiceMapFactory(new XmlContainerResolver(null)))->create()); } public function testGetPrivateService(): void diff --git a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php index b3a8fa31..3c40d61b 100644 --- a/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php +++ b/tests/Rules/Symfony/ContainerInterfacePrivateServiceRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Symfony; use PHPStan\Rules\Rule; +use PHPStan\Symfony\XmlContainerResolver; use PHPStan\Symfony\XmlServiceMapFactory; use PHPStan\Testing\RuleTestCase; @@ -14,7 +15,7 @@ final class ContainerInterfacePrivateServiceRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ContainerInterfacePrivateServiceRule((new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create()); + return new ContainerInterfacePrivateServiceRule((new XmlServiceMapFactory(new XmlContainerResolver(__DIR__ . '/container.xml')))->create()); } public function testGetPrivateService(): void diff --git a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php index 33e3015d..f03f3afd 100644 --- a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php +++ b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleFakeTest.php @@ -4,6 +4,7 @@ use PhpParser\PrettyPrinter\Standard; use PHPStan\Rules\Rule; +use PHPStan\Symfony\XmlContainerResolver; use PHPStan\Symfony\XmlServiceMapFactory; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Symfony\ServiceTypeSpecifyingExtension; @@ -16,7 +17,7 @@ final class ContainerInterfaceUnknownServiceRuleFakeTest extends RuleTestCase protected function getRule(): Rule { - return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(null))->create(), new Standard()); + return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(new XmlContainerResolver(null)))->create(), new Standard()); } /** diff --git a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php index 306e219a..1529c9e2 100644 --- a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php +++ b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php @@ -4,6 +4,7 @@ use PhpParser\PrettyPrinter\Standard; use PHPStan\Rules\Rule; +use PHPStan\Symfony\XmlContainerResolver; use PHPStan\Symfony\XmlServiceMapFactory; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Symfony\ServiceTypeSpecifyingExtension; @@ -17,7 +18,7 @@ final class ContainerInterfaceUnknownServiceRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(__DIR__ . '/container.xml'))->create(), new Standard()); + return new ContainerInterfaceUnknownServiceRule((new XmlServiceMapFactory(new XmlContainerResolver(__DIR__ . '/container.xml')))->create(), new Standard()); } /** diff --git a/tests/Rules/Symfony/container_loader.php b/tests/Rules/Symfony/container_loader.php new file mode 100644 index 00000000..d6a0c801 --- /dev/null +++ b/tests/Rules/Symfony/container_loader.php @@ -0,0 +1,3 @@ +create()->getParameter($key)); } public function testGetParameterEscapedPath(): void { - $factory = new XmlParameterMapFactory(__DIR__ . '/containers/bugfix%2Fcontainer.xml'); + $factory = new XmlParameterMapFactory(new XmlContainerResolver(__DIR__ . '/containers/bugfix%2Fcontainer.xml')); $serviceMap = $factory->create(); self::assertNotNull($serviceMap->getParameter('app.string')); diff --git a/tests/Symfony/DefaultServiceMapTest.php b/tests/Symfony/DefaultServiceMapTest.php index 35d6b35d..b97fec38 100644 --- a/tests/Symfony/DefaultServiceMapTest.php +++ b/tests/Symfony/DefaultServiceMapTest.php @@ -13,13 +13,13 @@ final class DefaultServiceMapTest extends TestCase */ public function testGetService(string $id, callable $validator): void { - $factory = new XmlServiceMapFactory(__DIR__ . '/container.xml'); + $factory = new XmlServiceMapFactory(new XmlContainerResolver(__DIR__ . '/container.xml')); $validator($factory->create()->getService($id)); } public function testGetContainerEscapedPath(): void { - $factory = new XmlServiceMapFactory(__DIR__ . '/containers/bugfix%2Fcontainer.xml'); + $factory = new XmlServiceMapFactory(new XmlContainerResolver(__DIR__ . '/containers/bugfix%2Fcontainer.xml')); $serviceMap = $factory->create(); self::assertNotNull($serviceMap->getService('withClass'));