From d5c083f02d128e0f98c7d08703120e9bbacdeb9a Mon Sep 17 00:00:00 2001 From: Greg Korba Date: Tue, 7 Jan 2025 16:51:12 +0100 Subject: [PATCH 1/7] Initial implementation of `SymfonyContainerResultCacheMetaExtension` --- composer.json | 2 +- extension.neon | 5 ++ ...mfonyContainerResultCacheMetaExtension.php | 54 +++++++++++++++++++ src/Symfony/XmlParameterMapFactory.php | 11 +++- src/Symfony/XmlServiceMapFactory.php | 11 +++- 5 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/SymfonyContainerResultCacheMetaExtension.php diff --git a/composer.json b/composer.json index 6cf39b10..a832e1bd 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "require": { "php": "^7.4 || ^8.0", "ext-simplexml": "*", - "phpstan/phpstan": "^2.0" + "phpstan/phpstan": "^2.1.2" }, "conflict": { "symfony/framework-bundle": "<3.0" diff --git a/extension.neon b/extension.neon index 06580626..34c7d889 100644 --- a/extension.neon +++ b/extension.neon @@ -363,3 +363,8 @@ services: - factory: PHPStan\Type\Symfony\ExtensionGetConfigurationReturnTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + - + class: PHPStan\Symfony\SymfonyContainerResultCacheMetaExtension + tags: + - phpstan.resultCacheMetaExtension diff --git a/src/Symfony/SymfonyContainerResultCacheMetaExtension.php b/src/Symfony/SymfonyContainerResultCacheMetaExtension.php new file mode 100644 index 00000000..a9502da2 --- /dev/null +++ b/src/Symfony/SymfonyContainerResultCacheMetaExtension.php @@ -0,0 +1,54 @@ +parameterMapFactory = $parameterMapFactory; + $this->serviceMapFactory = $serviceMapFactory; + } + + public function getKey(): string + { + return 'symfonyDiContainer'; + } + + public function getHash(): string + { + return hash('sha256', serialize([ + 'parameters' => array_map( + static fn (ParameterDefinition $parameter) => [ + 'name' => $parameter->getKey(), + 'value' => $parameter->getValue(), + ], + $this->parameterMapFactory->create()->getParameters(), + ), + 'services' => array_map( + static fn (ServiceDefinition $service) => [ + 'id' => $service->getId(), + 'class' => $service->getClass(), + 'public' => $service->isPublic() ? 'yes' : 'no', + 'synthetic' => $service->isSynthetic() ? 'yes' : 'no', + 'alias' => $service->getAlias(), + ], + $this->serviceMapFactory->create()->getServices(), + ), + ])); + } + +} diff --git a/src/Symfony/XmlParameterMapFactory.php b/src/Symfony/XmlParameterMapFactory.php index b893308f..41aa8cc1 100644 --- a/src/Symfony/XmlParameterMapFactory.php +++ b/src/Symfony/XmlParameterMapFactory.php @@ -8,6 +8,7 @@ use function base64_decode; use function file_get_contents; use function is_numeric; +use function ksort; use function simplexml_load_string; use function sprintf; use function strpos; @@ -15,6 +16,8 @@ final class XmlParameterMapFactory implements ParameterMapFactory { + private ?ParameterMap $parameterMap = null; + private ?string $containerXml = null; public function __construct(?string $containerXmlPath) @@ -24,6 +27,10 @@ public function __construct(?string $containerXmlPath) public function create(): ParameterMap { + if ($this->parameterMap !== null) { + return $this->parameterMap; + } + if ($this->containerXml === null) { return new FakeParameterMap(); } @@ -52,7 +59,9 @@ public function create(): ParameterMap $parameters[$parameter->getKey()] = $parameter; } - return new DefaultParameterMap($parameters); + ksort($parameters); + + return $this->parameterMap = new DefaultParameterMap($parameters); } /** diff --git a/src/Symfony/XmlServiceMapFactory.php b/src/Symfony/XmlServiceMapFactory.php index 734c22c7..87f8b38a 100644 --- a/src/Symfony/XmlServiceMapFactory.php +++ b/src/Symfony/XmlServiceMapFactory.php @@ -4,6 +4,7 @@ use SimpleXMLElement; use function file_get_contents; +use function ksort; use function simplexml_load_string; use function sprintf; use function strpos; @@ -12,6 +13,8 @@ final class XmlServiceMapFactory implements ServiceMapFactory { + private ?ServiceMap $serviceMap = null; + private ?string $containerXml = null; public function __construct(?string $containerXmlPath) @@ -21,6 +24,10 @@ public function __construct(?string $containerXmlPath) public function create(): ServiceMap { + if ($this->serviceMap !== null) { + return $this->serviceMap; + } + if ($this->containerXml === null) { return new FakeServiceMap(); } @@ -85,7 +92,9 @@ public function create(): ServiceMap ); } - return new DefaultServiceMap($services); + ksort($services); + + return $this->serviceMap = new DefaultServiceMap($services); } private function cleanServiceId(string $id): string From d19172cd30b9654ce2a79aaa54c17aded04bac0a Mon Sep 17 00:00:00 2001 From: Greg Korba Date: Wed, 8 Jan 2025 19:15:14 +0100 Subject: [PATCH 2/7] Inject parameter/service maps directly instead of factories --- ...SymfonyContainerResultCacheMetaExtension.php | 17 +++++++---------- src/Symfony/XmlParameterMapFactory.php | 8 +------- src/Symfony/XmlServiceMapFactory.php | 8 +------- 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/src/Symfony/SymfonyContainerResultCacheMetaExtension.php b/src/Symfony/SymfonyContainerResultCacheMetaExtension.php index a9502da2..bc2cf67d 100644 --- a/src/Symfony/SymfonyContainerResultCacheMetaExtension.php +++ b/src/Symfony/SymfonyContainerResultCacheMetaExtension.php @@ -10,17 +10,14 @@ final class SymfonyContainerResultCacheMetaExtension implements ResultCacheMetaExtension { - private ParameterMapFactory $parameterMapFactory; + private ParameterMap $parameterMap; - private ServiceMapFactory $serviceMapFactory; + private ServiceMap $serviceMap; - public function __construct( - ParameterMapFactory $parameterMapFactory, - ServiceMapFactory $serviceMapFactory - ) + public function __construct(ParameterMap $parameterMap, ServiceMap $serviceMap) { - $this->parameterMapFactory = $parameterMapFactory; - $this->serviceMapFactory = $serviceMapFactory; + $this->parameterMap = $parameterMap; + $this->serviceMap = $serviceMap; } public function getKey(): string @@ -36,7 +33,7 @@ public function getHash(): string 'name' => $parameter->getKey(), 'value' => $parameter->getValue(), ], - $this->parameterMapFactory->create()->getParameters(), + $this->parameterMap->getParameters(), ), 'services' => array_map( static fn (ServiceDefinition $service) => [ @@ -46,7 +43,7 @@ public function getHash(): string 'synthetic' => $service->isSynthetic() ? 'yes' : 'no', 'alias' => $service->getAlias(), ], - $this->serviceMapFactory->create()->getServices(), + $this->serviceMap->getServices(), ), ])); } diff --git a/src/Symfony/XmlParameterMapFactory.php b/src/Symfony/XmlParameterMapFactory.php index 41aa8cc1..c7503312 100644 --- a/src/Symfony/XmlParameterMapFactory.php +++ b/src/Symfony/XmlParameterMapFactory.php @@ -16,8 +16,6 @@ final class XmlParameterMapFactory implements ParameterMapFactory { - private ?ParameterMap $parameterMap = null; - private ?string $containerXml = null; public function __construct(?string $containerXmlPath) @@ -27,10 +25,6 @@ public function __construct(?string $containerXmlPath) public function create(): ParameterMap { - if ($this->parameterMap !== null) { - return $this->parameterMap; - } - if ($this->containerXml === null) { return new FakeParameterMap(); } @@ -61,7 +55,7 @@ public function create(): ParameterMap ksort($parameters); - return $this->parameterMap = new DefaultParameterMap($parameters); + return new DefaultParameterMap($parameters); } /** diff --git a/src/Symfony/XmlServiceMapFactory.php b/src/Symfony/XmlServiceMapFactory.php index 87f8b38a..b24f3730 100644 --- a/src/Symfony/XmlServiceMapFactory.php +++ b/src/Symfony/XmlServiceMapFactory.php @@ -13,8 +13,6 @@ final class XmlServiceMapFactory implements ServiceMapFactory { - private ?ServiceMap $serviceMap = null; - private ?string $containerXml = null; public function __construct(?string $containerXmlPath) @@ -24,10 +22,6 @@ public function __construct(?string $containerXmlPath) public function create(): ServiceMap { - if ($this->serviceMap !== null) { - return $this->serviceMap; - } - if ($this->containerXml === null) { return new FakeServiceMap(); } @@ -94,7 +88,7 @@ public function create(): ServiceMap ksort($services); - return $this->serviceMap = new DefaultServiceMap($services); + return new DefaultServiceMap($services); } private function cleanServiceId(string $id): string From c9f3147991f0b96078786ecc30fd04b25121be12 Mon Sep 17 00:00:00 2001 From: Greg Korba Date: Wed, 8 Jan 2025 19:17:56 +0100 Subject: [PATCH 3/7] Process parameters/services only if they were found in XML file Otherwise it may fail with `foreach() argument must be of type array|object, null given`. --- src/Symfony/XmlParameterMapFactory.php | 20 +++++---- src/Symfony/XmlServiceMapFactory.php | 60 ++++++++++++++------------ 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/Symfony/XmlParameterMapFactory.php b/src/Symfony/XmlParameterMapFactory.php index c7503312..4d3d3578 100644 --- a/src/Symfony/XmlParameterMapFactory.php +++ b/src/Symfony/XmlParameterMapFactory.php @@ -6,6 +6,7 @@ use PHPStan\ShouldNotHappenException; use SimpleXMLElement; use function base64_decode; +use function count; use function file_get_contents; use function is_numeric; use function ksort; @@ -41,16 +42,19 @@ public function create(): ParameterMap /** @var Parameter[] $parameters */ $parameters = []; - foreach ($xml->parameters->parameter as $def) { - /** @var SimpleXMLElement $attrs */ - $attrs = $def->attributes(); - $parameter = new Parameter( - (string) $attrs->key, - $this->getNodeValue($def), - ); + if (count($xml->parameters) > 0) { + foreach ($xml->parameters->parameter as $def) { + /** @var SimpleXMLElement $attrs */ + $attrs = $def->attributes(); - $parameters[$parameter->getKey()] = $parameter; + $parameter = new Parameter( + (string) $attrs->key, + $this->getNodeValue($def), + ); + + $parameters[$parameter->getKey()] = $parameter; + } } ksort($parameters); diff --git a/src/Symfony/XmlServiceMapFactory.php b/src/Symfony/XmlServiceMapFactory.php index b24f3730..ac79cb30 100644 --- a/src/Symfony/XmlServiceMapFactory.php +++ b/src/Symfony/XmlServiceMapFactory.php @@ -3,6 +3,7 @@ namespace PHPStan\Symfony; use SimpleXMLElement; +use function count; use function file_get_contents; use function ksort; use function simplexml_load_string; @@ -40,35 +41,38 @@ public function create(): ServiceMap $services = []; /** @var Service[] $aliases */ $aliases = []; - foreach ($xml->services->service as $def) { - /** @var SimpleXMLElement $attrs */ - $attrs = $def->attributes(); - if (!isset($attrs->id)) { - continue; - } - - $serviceTags = []; - foreach ($def->tag as $tag) { - $tagAttrs = ((array) $tag->attributes())['@attributes'] ?? []; - $tagName = $tagAttrs['name']; - unset($tagAttrs['name']); - - $serviceTags[] = new ServiceTag($tagName, $tagAttrs); - } - - $service = new Service( - $this->cleanServiceId((string) $attrs->id), - isset($attrs->class) ? (string) $attrs->class : null, - isset($attrs->public) && (string) $attrs->public === 'true', - isset($attrs->synthetic) && (string) $attrs->synthetic === 'true', - isset($attrs->alias) ? $this->cleanServiceId((string) $attrs->alias) : null, - $serviceTags, - ); - if ($service->getAlias() !== null) { - $aliases[] = $service; - } else { - $services[$service->getId()] = $service; + if (count($xml->services) > 0) { + foreach ($xml->services->service as $def) { + /** @var SimpleXMLElement $attrs */ + $attrs = $def->attributes(); + if (!isset($attrs->id)) { + continue; + } + + $serviceTags = []; + foreach ($def->tag as $tag) { + $tagAttrs = ((array) $tag->attributes())['@attributes'] ?? []; + $tagName = $tagAttrs['name']; + unset($tagAttrs['name']); + + $serviceTags[] = new ServiceTag($tagName, $tagAttrs); + } + + $service = new Service( + $this->cleanServiceId((string) $attrs->id), + isset($attrs->class) ? (string) $attrs->class : null, + isset($attrs->public) && (string) $attrs->public === 'true', + isset($attrs->synthetic) && (string) $attrs->synthetic === 'true', + isset($attrs->alias) ? $this->cleanServiceId((string) $attrs->alias) : null, + $serviceTags, + ); + + if ($service->getAlias() !== null) { + $aliases[] = $service; + } else { + $services[$service->getId()] = $service; + } } } foreach ($aliases as $service) { From 43d493b66a4fd09ea14a931d109f72bbba88fbd4 Mon Sep 17 00:00:00 2001 From: Greg Korba Date: Wed, 8 Jan 2025 19:40:17 +0100 Subject: [PATCH 4/7] Add test for `SymfonyContainerResultCacheMetaExtension` This test ensures that hash calculated for Symfony's DI container remains the same or changes under provided conditions. This test is significantly slower than other unit tests, this is caused by rebuilding Nette container for each Symfony DI container's XML content - it's required in order to get fresh parameter/service maps from `self::getContainer()->getByType()`, because `self::getContainer()` caches containers for each `self::getAdditionalConfigFiles()` unique set, and with the same Nette config/container it would be retrieved from cache, so the hash correctness couldn't be verified properly. --- ...yContainerResultCacheMetaExtensionTest.php | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php diff --git a/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php b/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php new file mode 100644 index 00000000..e0e8f346 --- /dev/null +++ b/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php @@ -0,0 +1,304 @@ + $sameHashXmlContents + * + * @dataProvider provideContainerHashIsCalculatedCorrectlyCases + */ + public function testContainerHashIsCalculatedCorrectly( + array $sameHashXmlContents, + string $invalidatingXmlContent + ): void + { + $hash = null; + + self::assertGreaterThan(0, count($sameHashXmlContents)); + + foreach ($sameHashXmlContents as $xmlContent) { + $currentHash = $this->calculateSymfonyContainerHash($xmlContent); + + if ($hash === null) { + $hash = $currentHash; + } else { + self::assertSame($hash, $currentHash); + } + } + + self::assertNotSame($hash, $this->calculateSymfonyContainerHash($invalidatingXmlContent)); + } + + /** + * @return iterable, string}> + */ + public static function provideContainerHashIsCalculatedCorrectlyCases(): iterable + { + yield 'service "class" changes' => [ + [ + <<<'XML' + + + + + + + XML, + // Swapping services order in XML file does not affect the calculated hash + <<<'XML' + + + + + + + XML, + ], + <<<'XML' + + + + + + + XML, + ]; + + yield 'service visibility changes' => [ + [ + <<<'XML' + + + + + + XML, + // Placement of XML attributes does not affect the calculated hash + <<<'XML' + + + + + + XML, + ], + <<<'XML' + + + + + + XML, + ]; + + yield 'service syntheticity changes' => [ + [ + <<<'XML' + + + + + + XML, + ], + <<<'XML' + + + + + + XML, + ]; + + yield 'service alias changes' => [ + [ + <<<'XML' + + + + + + + + XML, + <<<'XML' + + + + + + + + XML, + ], + <<<'XML' + + + + + + + + XML, + ]; + + yield 'new service added' => [ + [ + <<<'XML' + + + + + + XML, + ], + <<<'XML' + + + + + + + XML, + ]; + + yield 'service removed' => [ + [ + <<<'XML' + + + + + + + XML, + ], + <<<'XML' + + + + + + XML, + ]; + + yield 'parameter value changes' => [ + [ + <<<'XML' + + + foo + bar + + + XML, + // Swapping parameters order in XML file does not affect the calculated hash + <<<'XML' + + + bar + foo + + + XML, + ], + <<<'XML' + + + foo + buzz + + + XML, + ]; + + yield 'new parameter added' => [ + [ + <<<'XML' + + + foo + + + XML, + ], + <<<'XML' + + + foo + bar + + + XML, + ]; + + yield 'parameter removed' => [ + [ + <<<'XML' + + + foo + bar + + + XML, + ], + <<<'XML' + + + foo + + + XML, + ]; + } + + private function calculateSymfonyContainerHash(string $xmlContent): string + { + $symfonyContainerXmlPath = tempnam(sys_get_temp_dir(), 'phpstan-meta-extension-test-container-xml-'); + self::$configFilePath = tempnam(sys_get_temp_dir(), 'phpstan-meta-extension-test-config-') . '.neon'; + + file_put_contents( + self::$configFilePath, + <<getByType(ParameterMap::class), + self::getContainer()->getByType(ServiceMap::class), + ); + + return $metaExtension->getHash(); + } + +} From 858d7473d3e5ef0f824ebd40203411a89da8b8a5 Mon Sep 17 00:00:00 2001 From: Greg Korba Date: Wed, 8 Jan 2025 20:13:44 +0100 Subject: [PATCH 5/7] Calculate hash properly also taking services' tags into consideration --- ...mfonyContainerResultCacheMetaExtension.php | 29 +++++-- ...yContainerResultCacheMetaExtensionTest.php | 83 +++++++++++++++++++ 2 files changed, 104 insertions(+), 8 deletions(-) diff --git a/src/Symfony/SymfonyContainerResultCacheMetaExtension.php b/src/Symfony/SymfonyContainerResultCacheMetaExtension.php index bc2cf67d..1f93d7d3 100644 --- a/src/Symfony/SymfonyContainerResultCacheMetaExtension.php +++ b/src/Symfony/SymfonyContainerResultCacheMetaExtension.php @@ -6,6 +6,7 @@ use function array_map; use function hash; use function serialize; +use function sort; final class SymfonyContainerResultCacheMetaExtension implements ResultCacheMetaExtension { @@ -29,20 +30,32 @@ public function getHash(): string { return hash('sha256', serialize([ 'parameters' => array_map( - static fn (ParameterDefinition $parameter) => [ + static fn (ParameterDefinition $parameter): array => [ 'name' => $parameter->getKey(), 'value' => $parameter->getValue(), ], $this->parameterMap->getParameters(), ), 'services' => array_map( - static fn (ServiceDefinition $service) => [ - 'id' => $service->getId(), - 'class' => $service->getClass(), - 'public' => $service->isPublic() ? 'yes' : 'no', - 'synthetic' => $service->isSynthetic() ? 'yes' : 'no', - 'alias' => $service->getAlias(), - ], + static function (ServiceDefinition $service): array { + $serviceTags = array_map( + static fn (ServiceTag $tag) => [ + 'name' => $tag->getName(), + 'attributes' => $tag->getAttributes(), + ], + $service->getTags(), + ); + sort($serviceTags); + + return [ + 'id' => $service->getId(), + 'class' => $service->getClass(), + 'public' => $service->isPublic() ? 'yes' : 'no', + 'synthetic' => $service->isSynthetic() ? 'yes' : 'no', + 'alias' => $service->getAlias(), + 'tags' => $serviceTags, + ]; + }, $this->serviceMap->getServices(), ), ])); diff --git a/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php b/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php index e0e8f346..d90b5a97 100644 --- a/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php +++ b/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php @@ -167,6 +167,89 @@ public static function provideContainerHashIsCalculatedCorrectlyCases(): iterabl XML, ]; + yield 'service tag attributes changes' => [ + [ + <<<'XML' + + + + + + + + + XML, + <<<'XML' + + + + + + + + + XML, + ], + <<<'XML' + + + + + + + + + XML, + ]; + + yield 'service tag added' => [ + [ + <<<'XML' + + + + + + + + XML, + ], + <<<'XML' + + + + + + + + + XML, + ]; + + yield 'service tag removed' => [ + [ + <<<'XML' + + + + + + + + + XML, + ], + <<<'XML' + + + + + + + + XML, + ]; + yield 'new service added' => [ [ <<<'XML' From 192e11e81273c4366918580f266805e7aa62b9bc Mon Sep 17 00:00:00 2001 From: Greg Korba Date: Tue, 21 Jan 2025 17:08:30 +0100 Subject: [PATCH 6/7] Simplify tests by omitting I/O operations --- ...mfonyContainerResultCacheMetaExtension.php | 58 +-- ...yContainerResultCacheMetaExtensionTest.php | 487 +++++++----------- 2 files changed, 225 insertions(+), 320 deletions(-) diff --git a/src/Symfony/SymfonyContainerResultCacheMetaExtension.php b/src/Symfony/SymfonyContainerResultCacheMetaExtension.php index 1f93d7d3..7d3d78b8 100644 --- a/src/Symfony/SymfonyContainerResultCacheMetaExtension.php +++ b/src/Symfony/SymfonyContainerResultCacheMetaExtension.php @@ -5,6 +5,7 @@ use PHPStan\Analyser\ResultCache\ResultCacheMetaExtension; use function array_map; use function hash; +use function ksort; use function serialize; use function sort; @@ -28,37 +29,34 @@ public function getKey(): string public function getHash(): string { - return hash('sha256', serialize([ - 'parameters' => array_map( - static fn (ParameterDefinition $parameter): array => [ - 'name' => $parameter->getKey(), - 'value' => $parameter->getValue(), + $services = $parameters = []; + + foreach ($this->parameterMap->getParameters() as $parameter) { + $parameters[$parameter->getKey()] = $parameter->getValue(); + } + ksort($parameters); + + foreach ($this->serviceMap->getServices() as $service) { + $serviceTags = array_map( + static fn (ServiceTag $tag) => [ + 'name' => $tag->getName(), + 'attributes' => $tag->getAttributes(), ], - $this->parameterMap->getParameters(), - ), - 'services' => array_map( - static function (ServiceDefinition $service): array { - $serviceTags = array_map( - static fn (ServiceTag $tag) => [ - 'name' => $tag->getName(), - 'attributes' => $tag->getAttributes(), - ], - $service->getTags(), - ); - sort($serviceTags); - - return [ - 'id' => $service->getId(), - 'class' => $service->getClass(), - 'public' => $service->isPublic() ? 'yes' : 'no', - 'synthetic' => $service->isSynthetic() ? 'yes' : 'no', - 'alias' => $service->getAlias(), - 'tags' => $serviceTags, - ]; - }, - $this->serviceMap->getServices(), - ), - ])); + $service->getTags(), + ); + sort($serviceTags); + + $services[$service->getId()] = [ + 'class' => $service->getClass(), + 'public' => $service->isPublic() ? 'yes' : 'no', + 'synthetic' => $service->isSynthetic() ? 'yes' : 'no', + 'alias' => $service->getAlias(), + 'tags' => $serviceTags, + ]; + } + ksort($services); + + return hash('sha256', serialize(['parameters' => $parameters, 'services' => $services])); } } diff --git a/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php b/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php index d90b5a97..f5c8503f 100644 --- a/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php +++ b/tests/Symfony/SymfonyContainerResultCacheMetaExtensionTest.php @@ -4,45 +4,33 @@ use PHPStan\Testing\PHPStanTestCase; use function count; -use function file_put_contents; -use function sys_get_temp_dir; -use function tempnam; +/** + * @phpstan-type ContainerContents array{parameters?: ParameterMap, services?: ServiceMap} + */ final class SymfonyContainerResultCacheMetaExtensionTest extends PHPStanTestCase { - private static string $configFilePath; - /** - * This test has to check if hash of the Symfony Container is correctly calculated, - * in order to do that we need to dynamically create a temporary configuration file - * because `PHPStanTestCase::getContainer()` caches container under key calculated from - * additional config files' paths, so we can't reuse the same config file between tests. - */ - public static function getAdditionalConfigFiles(): array - { - return [ - __DIR__ . '/../../extension.neon', - self::$configFilePath, - ]; - } - - /** - * @param list $sameHashXmlContents + * @param list $sameHashContents + * @param ContainerContents $invalidatingContent * * @dataProvider provideContainerHashIsCalculatedCorrectlyCases */ public function testContainerHashIsCalculatedCorrectly( - array $sameHashXmlContents, - string $invalidatingXmlContent + array $sameHashContents, + array $invalidatingContent ): void { $hash = null; - self::assertGreaterThan(0, count($sameHashXmlContents)); + self::assertGreaterThan(0, count($sameHashContents)); - foreach ($sameHashXmlContents as $xmlContent) { - $currentHash = $this->calculateSymfonyContainerHash($xmlContent); + foreach ($sameHashContents as $content) { + $currentHash = (new SymfonyContainerResultCacheMetaExtension( + $content['parameters'] ?? new DefaultParameterMap([]), + $content['services'] ?? new DefaultServiceMap([]), + ))->getHash(); if ($hash === null) { $hash = $currentHash; @@ -51,337 +39,256 @@ public function testContainerHashIsCalculatedCorrectly( } } - self::assertNotSame($hash, $this->calculateSymfonyContainerHash($invalidatingXmlContent)); + self::assertNotSame( + $hash, + (new SymfonyContainerResultCacheMetaExtension( + $invalidatingContent['parameters'] ?? new DefaultParameterMap([]), + $invalidatingContent['services'] ?? new DefaultServiceMap([]), + ))->getHash(), + ); } /** - * @return iterable, string}> + * @return iterable, ContainerContents}> */ public static function provideContainerHashIsCalculatedCorrectlyCases(): iterable { yield 'service "class" changes' => [ [ - <<<'XML' - - - - - - - XML, + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + new Service('Bar', 'Bar', true, false, null), + ]), + ], // Swapping services order in XML file does not affect the calculated hash - <<<'XML' - - - - - - - XML, + [ + 'services' => new DefaultServiceMap([ + new Service('Bar', 'Bar', true, false, null), + new Service('Foo', 'Foo', true, false, null), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + new Service('Bar', 'BarAdapter', true, false, null), + ]), ], - <<<'XML' - - - - - - - XML, ]; yield 'service visibility changes' => [ [ - <<<'XML' - - - - - - XML, - // Placement of XML attributes does not affect the calculated hash - <<<'XML' - - - - - - XML, + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', false, false, null), + ]), ], - <<<'XML' - - - - - - XML, ]; yield 'service syntheticity changes' => [ [ - <<<'XML' - - - - - - XML, + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, true, null), + ]), ], - <<<'XML' - - - - - - XML, ]; yield 'service alias changes' => [ [ - <<<'XML' - - - - - - - - XML, - <<<'XML' - - - - - - - - XML, + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + new Service('Bar', 'Bar', true, false, null), + new Service('Baz', null, true, false, 'Foo'), + ]), + ], + // Swapping services order in XML file does not affect the calculated hash + [ + 'services' => new DefaultServiceMap([ + new Service('Baz', null, true, false, 'Foo'), + new Service('Bar', 'Bar', true, false, null), + new Service('Foo', 'Foo', true, false, null), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + new Service('Bar', 'Bar', true, false, null), + new Service('Baz', null, true, false, 'Bar'), + ]), ], - <<<'XML' - - - - - - - - XML, ]; yield 'service tag attributes changes' => [ [ - <<<'XML' - - - - - - - - - XML, - <<<'XML' - - - - - - - - - XML, + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null, [ + new ServiceTag('foo.bar', ['baz' => 'bar']), + new ServiceTag('foo.baz', ['baz' => 'baz']), + ]), + ]), + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null, [ + new ServiceTag('foo.baz', ['baz' => 'baz']), + new ServiceTag('foo.bar', ['baz' => 'bar']), + ]), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null, [ + new ServiceTag('foo.bar', ['baz' => 'bar']), + new ServiceTag('foo.baz', ['baz' => 'buzz']), + ]), + ]), ], - <<<'XML' - - - - - - - - - XML, ]; yield 'service tag added' => [ [ - <<<'XML' - - - - - - - - XML, + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null, [ + new ServiceTag('foo.bar', ['baz' => 'bar']), + ]), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null, [ + new ServiceTag('foo.bar', ['baz' => 'bar']), + new ServiceTag('foo.baz', ['baz' => 'baz']), + ]), + ]), ], - <<<'XML' - - - - - - - - - XML, ]; yield 'service tag removed' => [ [ - <<<'XML' - - - - - - - - - XML, + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null, [ + new ServiceTag('foo.bar', ['baz' => 'bar']), + new ServiceTag('foo.baz', ['baz' => 'baz']), + ]), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null, [ + new ServiceTag('foo.bar', ['baz' => 'bar']), + ]), + ]), ], - <<<'XML' - - - - - - - - XML, ]; yield 'new service added' => [ [ - <<<'XML' - - - - - - XML, + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + new Service('Bar', 'Bar', true, false, null), + ]), ], - <<<'XML' - - - - - - - XML, ]; yield 'service removed' => [ [ - <<<'XML' - - - - - - - XML, + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + new Service('Bar', 'Bar', true, false, null), + ]), + ], + ], + [ + 'services' => new DefaultServiceMap([ + new Service('Foo', 'Foo', true, false, null), + ]), ], - <<<'XML' - - - - - - XML, ]; yield 'parameter value changes' => [ [ - <<<'XML' - - - foo - bar - - - XML, - // Swapping parameters order in XML file does not affect the calculated hash - <<<'XML' - - - bar - foo - - - XML, + [ + 'parameters' => new DefaultParameterMap([ + new Parameter('foo', 'foo'), + new Parameter('bar', 'bar'), + ]), + ], + [ + 'parameters' => new DefaultParameterMap([ + new Parameter('bar', 'bar'), + new Parameter('foo', 'foo'), + ]), + ], + ], + [ + 'parameters' => new DefaultParameterMap([ + new Parameter('foo', 'foo'), + new Parameter('bar', 'buzz'), + ]), ], - <<<'XML' - - - foo - buzz - - - XML, ]; yield 'new parameter added' => [ [ - <<<'XML' - - - foo - - - XML, + [ + 'parameters' => new DefaultParameterMap([ + new Parameter('foo', 'foo'), + ]), + ], + ], + [ + 'parameters' => new DefaultParameterMap([ + new Parameter('foo', 'foo'), + new Parameter('bar', 'bar'), + ]), ], - <<<'XML' - - - foo - bar - - - XML, ]; yield 'parameter removed' => [ [ - <<<'XML' - - - foo - bar - - - XML, + [ + 'parameters' => new DefaultParameterMap([ + new Parameter('foo', 'foo'), + new Parameter('bar', 'bar'), + ]), + ], + ], + [ + 'parameters' => new DefaultParameterMap([ + new Parameter('foo', 'foo'), + ]), ], - <<<'XML' - - - foo - - - XML, ]; } - private function calculateSymfonyContainerHash(string $xmlContent): string - { - $symfonyContainerXmlPath = tempnam(sys_get_temp_dir(), 'phpstan-meta-extension-test-container-xml-'); - self::$configFilePath = tempnam(sys_get_temp_dir(), 'phpstan-meta-extension-test-config-') . '.neon'; - - file_put_contents( - self::$configFilePath, - <<getByType(ParameterMap::class), - self::getContainer()->getByType(ServiceMap::class), - ); - - return $metaExtension->getHash(); - } - } From ad830703e224ddf4afc8a4c3db2ea22cf2a8a304 Mon Sep 17 00:00:00 2001 From: Greg Korba Date: Tue, 21 Jan 2025 19:09:40 +0100 Subject: [PATCH 7/7] Use `var_export()` instead of `serialize()` --- src/Symfony/SymfonyContainerResultCacheMetaExtension.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/SymfonyContainerResultCacheMetaExtension.php b/src/Symfony/SymfonyContainerResultCacheMetaExtension.php index 7d3d78b8..8e2f8028 100644 --- a/src/Symfony/SymfonyContainerResultCacheMetaExtension.php +++ b/src/Symfony/SymfonyContainerResultCacheMetaExtension.php @@ -6,8 +6,8 @@ use function array_map; use function hash; use function ksort; -use function serialize; use function sort; +use function var_export; final class SymfonyContainerResultCacheMetaExtension implements ResultCacheMetaExtension { @@ -56,7 +56,7 @@ public function getHash(): string } ksort($services); - return hash('sha256', serialize(['parameters' => $parameters, 'services' => $services])); + return hash('sha256', var_export(['parameters' => $parameters, 'services' => $services], true)); } }