From 20f9c8249705211551f435fd66bcb107f4e0a2eb Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Mon, 17 May 2021 13:39:47 +0200 Subject: [PATCH] Dynamically load XML container via PHP This allows to automatically resolve the XML container configuration based on environment variables. It also has the benefit that it will automatically create the XML container file when it's not present (as it boots the container once). Fixes https://github.com/phpstan/phpstan-symfony/issues/105 --- README.md | 26 ++++++ extension.neon | 10 ++- src/Symfony/XmlContainerResolver.php | 55 +++++++++++++ src/Symfony/XmlParameterMapFactory.php | 24 ++---- src/Symfony/XmlServiceMapFactory.php | 26 ++---- ...terfacePrivateServicePhpLoaderRuleTest.php | 79 +++++++++++++++++++ ...nerInterfacePrivateServiceRuleFakeTest.php | 3 +- ...ntainerInterfacePrivateServiceRuleTest.php | 3 +- ...nerInterfaceUnknownServiceRuleFakeTest.php | 3 +- ...ntainerInterfaceUnknownServiceRuleTest.php | 3 +- tests/Rules/Symfony/container_loader.php | 3 + tests/Symfony/DefaultParameterMapTest.php | 4 +- tests/Symfony/DefaultServiceMapTest.php | 4 +- 13 files changed, 199 insertions(+), 44 deletions(-) create mode 100644 src/Symfony/XmlContainerResolver.php create mode 100644 tests/Rules/Symfony/ContainerInterfacePrivateServicePhpLoaderRuleTest.php create mode 100644 tests/Rules/Symfony/container_loader.php 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'));