Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 65f02c7

Browse files
authoredJan 21, 2025··
Add result cache meta extension for DI container
1 parent 7417f3a commit 65f02c7

File tree

6 files changed

+412
-37
lines changed

6 files changed

+412
-37
lines changed
 

‎composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"require": {
1616
"php": "^7.4 || ^8.0",
1717
"ext-simplexml": "*",
18-
"phpstan/phpstan": "^2.0"
18+
"phpstan/phpstan": "^2.1.2"
1919
},
2020
"conflict": {
2121
"symfony/framework-bundle": "<3.0"

‎extension.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,3 +363,8 @@ services:
363363
-
364364
factory: PHPStan\Type\Symfony\ExtensionGetConfigurationReturnTypeExtension
365365
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
366+
367+
-
368+
class: PHPStan\Symfony\SymfonyContainerResultCacheMetaExtension
369+
tags:
370+
- phpstan.resultCacheMetaExtension
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PHPStan\Analyser\ResultCache\ResultCacheMetaExtension;
6+
use function array_map;
7+
use function hash;
8+
use function ksort;
9+
use function sort;
10+
use function var_export;
11+
12+
final class SymfonyContainerResultCacheMetaExtension implements ResultCacheMetaExtension
13+
{
14+
15+
private ParameterMap $parameterMap;
16+
17+
private ServiceMap $serviceMap;
18+
19+
public function __construct(ParameterMap $parameterMap, ServiceMap $serviceMap)
20+
{
21+
$this->parameterMap = $parameterMap;
22+
$this->serviceMap = $serviceMap;
23+
}
24+
25+
public function getKey(): string
26+
{
27+
return 'symfonyDiContainer';
28+
}
29+
30+
public function getHash(): string
31+
{
32+
$services = $parameters = [];
33+
34+
foreach ($this->parameterMap->getParameters() as $parameter) {
35+
$parameters[$parameter->getKey()] = $parameter->getValue();
36+
}
37+
ksort($parameters);
38+
39+
foreach ($this->serviceMap->getServices() as $service) {
40+
$serviceTags = array_map(
41+
static fn (ServiceTag $tag) => [
42+
'name' => $tag->getName(),
43+
'attributes' => $tag->getAttributes(),
44+
],
45+
$service->getTags(),
46+
);
47+
sort($serviceTags);
48+
49+
$services[$service->getId()] = [
50+
'class' => $service->getClass(),
51+
'public' => $service->isPublic() ? 'yes' : 'no',
52+
'synthetic' => $service->isSynthetic() ? 'yes' : 'no',
53+
'alias' => $service->getAlias(),
54+
'tags' => $serviceTags,
55+
];
56+
}
57+
ksort($services);
58+
59+
return hash('sha256', var_export(['parameters' => $parameters, 'services' => $services], true));
60+
}
61+
62+
}

‎src/Symfony/XmlParameterMapFactory.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
use PHPStan\ShouldNotHappenException;
77
use SimpleXMLElement;
88
use function base64_decode;
9+
use function count;
910
use function file_get_contents;
1011
use function is_numeric;
12+
use function ksort;
1113
use function simplexml_load_string;
1214
use function sprintf;
1315
use function strpos;
@@ -40,18 +42,23 @@ public function create(): ParameterMap
4042

4143
/** @var Parameter[] $parameters */
4244
$parameters = [];
43-
foreach ($xml->parameters->parameter as $def) {
44-
/** @var SimpleXMLElement $attrs */
45-
$attrs = $def->attributes();
4645

47-
$parameter = new Parameter(
48-
(string) $attrs->key,
49-
$this->getNodeValue($def),
50-
);
46+
if (count($xml->parameters) > 0) {
47+
foreach ($xml->parameters->parameter as $def) {
48+
/** @var SimpleXMLElement $attrs */
49+
$attrs = $def->attributes();
5150

52-
$parameters[$parameter->getKey()] = $parameter;
51+
$parameter = new Parameter(
52+
(string) $attrs->key,
53+
$this->getNodeValue($def),
54+
);
55+
56+
$parameters[$parameter->getKey()] = $parameter;
57+
}
5358
}
5459

60+
ksort($parameters);
61+
5562
return new DefaultParameterMap($parameters);
5663
}
5764

‎src/Symfony/XmlServiceMapFactory.php

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
namespace PHPStan\Symfony;
44

55
use SimpleXMLElement;
6+
use function count;
67
use function file_get_contents;
8+
use function ksort;
79
use function simplexml_load_string;
810
use function sprintf;
911
use function strpos;
@@ -39,35 +41,38 @@ public function create(): ServiceMap
3941
$services = [];
4042
/** @var Service[] $aliases */
4143
$aliases = [];
42-
foreach ($xml->services->service as $def) {
43-
/** @var SimpleXMLElement $attrs */
44-
$attrs = $def->attributes();
45-
if (!isset($attrs->id)) {
46-
continue;
47-
}
48-
49-
$serviceTags = [];
50-
foreach ($def->tag as $tag) {
51-
$tagAttrs = ((array) $tag->attributes())['@attributes'] ?? [];
52-
$tagName = $tagAttrs['name'];
53-
unset($tagAttrs['name']);
54-
55-
$serviceTags[] = new ServiceTag($tagName, $tagAttrs);
56-
}
57-
58-
$service = new Service(
59-
$this->cleanServiceId((string) $attrs->id),
60-
isset($attrs->class) ? (string) $attrs->class : null,
61-
isset($attrs->public) && (string) $attrs->public === 'true',
62-
isset($attrs->synthetic) && (string) $attrs->synthetic === 'true',
63-
isset($attrs->alias) ? $this->cleanServiceId((string) $attrs->alias) : null,
64-
$serviceTags,
65-
);
6644

67-
if ($service->getAlias() !== null) {
68-
$aliases[] = $service;
69-
} else {
70-
$services[$service->getId()] = $service;
45+
if (count($xml->services) > 0) {
46+
foreach ($xml->services->service as $def) {
47+
/** @var SimpleXMLElement $attrs */
48+
$attrs = $def->attributes();
49+
if (!isset($attrs->id)) {
50+
continue;
51+
}
52+
53+
$serviceTags = [];
54+
foreach ($def->tag as $tag) {
55+
$tagAttrs = ((array) $tag->attributes())['@attributes'] ?? [];
56+
$tagName = $tagAttrs['name'];
57+
unset($tagAttrs['name']);
58+
59+
$serviceTags[] = new ServiceTag($tagName, $tagAttrs);
60+
}
61+
62+
$service = new Service(
63+
$this->cleanServiceId((string) $attrs->id),
64+
isset($attrs->class) ? (string) $attrs->class : null,
65+
isset($attrs->public) && (string) $attrs->public === 'true',
66+
isset($attrs->synthetic) && (string) $attrs->synthetic === 'true',
67+
isset($attrs->alias) ? $this->cleanServiceId((string) $attrs->alias) : null,
68+
$serviceTags,
69+
);
70+
71+
if ($service->getAlias() !== null) {
72+
$aliases[] = $service;
73+
} else {
74+
$services[$service->getId()] = $service;
75+
}
7176
}
7277
}
7378
foreach ($aliases as $service) {
@@ -85,6 +90,8 @@ public function create(): ServiceMap
8590
);
8691
}
8792

93+
ksort($services);
94+
8895
return new DefaultServiceMap($services);
8996
}
9097

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PHPStan\Testing\PHPStanTestCase;
6+
use function count;
7+
8+
/**
9+
* @phpstan-type ContainerContents array{parameters?: ParameterMap, services?: ServiceMap}
10+
*/
11+
final class SymfonyContainerResultCacheMetaExtensionTest extends PHPStanTestCase
12+
{
13+
14+
/**
15+
* @param list<ContainerContents> $sameHashContents
16+
* @param ContainerContents $invalidatingContent
17+
*
18+
* @dataProvider provideContainerHashIsCalculatedCorrectlyCases
19+
*/
20+
public function testContainerHashIsCalculatedCorrectly(
21+
array $sameHashContents,
22+
array $invalidatingContent
23+
): void
24+
{
25+
$hash = null;
26+
27+
self::assertGreaterThan(0, count($sameHashContents));
28+
29+
foreach ($sameHashContents as $content) {
30+
$currentHash = (new SymfonyContainerResultCacheMetaExtension(
31+
$content['parameters'] ?? new DefaultParameterMap([]),
32+
$content['services'] ?? new DefaultServiceMap([]),
33+
))->getHash();
34+
35+
if ($hash === null) {
36+
$hash = $currentHash;
37+
} else {
38+
self::assertSame($hash, $currentHash);
39+
}
40+
}
41+
42+
self::assertNotSame(
43+
$hash,
44+
(new SymfonyContainerResultCacheMetaExtension(
45+
$invalidatingContent['parameters'] ?? new DefaultParameterMap([]),
46+
$invalidatingContent['services'] ?? new DefaultServiceMap([]),
47+
))->getHash(),
48+
);
49+
}
50+
51+
/**
52+
* @return iterable<string, array{list<ContainerContents>, ContainerContents}>
53+
*/
54+
public static function provideContainerHashIsCalculatedCorrectlyCases(): iterable
55+
{
56+
yield 'service "class" changes' => [
57+
[
58+
[
59+
'services' => new DefaultServiceMap([
60+
new Service('Foo', 'Foo', true, false, null),
61+
new Service('Bar', 'Bar', true, false, null),
62+
]),
63+
],
64+
// Swapping services order in XML file does not affect the calculated hash
65+
[
66+
'services' => new DefaultServiceMap([
67+
new Service('Bar', 'Bar', true, false, null),
68+
new Service('Foo', 'Foo', true, false, null),
69+
]),
70+
],
71+
],
72+
[
73+
'services' => new DefaultServiceMap([
74+
new Service('Foo', 'Foo', true, false, null),
75+
new Service('Bar', 'BarAdapter', true, false, null),
76+
]),
77+
],
78+
];
79+
80+
yield 'service visibility changes' => [
81+
[
82+
[
83+
'services' => new DefaultServiceMap([
84+
new Service('Foo', 'Foo', true, false, null),
85+
]),
86+
],
87+
],
88+
[
89+
'services' => new DefaultServiceMap([
90+
new Service('Foo', 'Foo', false, false, null),
91+
]),
92+
],
93+
];
94+
95+
yield 'service syntheticity changes' => [
96+
[
97+
[
98+
'services' => new DefaultServiceMap([
99+
new Service('Foo', 'Foo', true, false, null),
100+
]),
101+
],
102+
],
103+
[
104+
'services' => new DefaultServiceMap([
105+
new Service('Foo', 'Foo', true, true, null),
106+
]),
107+
],
108+
];
109+
110+
yield 'service alias changes' => [
111+
[
112+
[
113+
'services' => new DefaultServiceMap([
114+
new Service('Foo', 'Foo', true, false, null),
115+
new Service('Bar', 'Bar', true, false, null),
116+
new Service('Baz', null, true, false, 'Foo'),
117+
]),
118+
],
119+
// Swapping services order in XML file does not affect the calculated hash
120+
[
121+
'services' => new DefaultServiceMap([
122+
new Service('Baz', null, true, false, 'Foo'),
123+
new Service('Bar', 'Bar', true, false, null),
124+
new Service('Foo', 'Foo', true, false, null),
125+
]),
126+
],
127+
],
128+
[
129+
'services' => new DefaultServiceMap([
130+
new Service('Foo', 'Foo', true, false, null),
131+
new Service('Bar', 'Bar', true, false, null),
132+
new Service('Baz', null, true, false, 'Bar'),
133+
]),
134+
],
135+
];
136+
137+
yield 'service tag attributes changes' => [
138+
[
139+
[
140+
'services' => new DefaultServiceMap([
141+
new Service('Foo', 'Foo', true, false, null, [
142+
new ServiceTag('foo.bar', ['baz' => 'bar']),
143+
new ServiceTag('foo.baz', ['baz' => 'baz']),
144+
]),
145+
]),
146+
],
147+
[
148+
'services' => new DefaultServiceMap([
149+
new Service('Foo', 'Foo', true, false, null, [
150+
new ServiceTag('foo.baz', ['baz' => 'baz']),
151+
new ServiceTag('foo.bar', ['baz' => 'bar']),
152+
]),
153+
]),
154+
],
155+
],
156+
[
157+
'services' => new DefaultServiceMap([
158+
new Service('Foo', 'Foo', true, false, null, [
159+
new ServiceTag('foo.bar', ['baz' => 'bar']),
160+
new ServiceTag('foo.baz', ['baz' => 'buzz']),
161+
]),
162+
]),
163+
],
164+
];
165+
166+
yield 'service tag added' => [
167+
[
168+
[
169+
'services' => new DefaultServiceMap([
170+
new Service('Foo', 'Foo', true, false, null, [
171+
new ServiceTag('foo.bar', ['baz' => 'bar']),
172+
]),
173+
]),
174+
],
175+
],
176+
[
177+
'services' => new DefaultServiceMap([
178+
new Service('Foo', 'Foo', true, false, null, [
179+
new ServiceTag('foo.bar', ['baz' => 'bar']),
180+
new ServiceTag('foo.baz', ['baz' => 'baz']),
181+
]),
182+
]),
183+
],
184+
];
185+
186+
yield 'service tag removed' => [
187+
[
188+
[
189+
'services' => new DefaultServiceMap([
190+
new Service('Foo', 'Foo', true, false, null, [
191+
new ServiceTag('foo.bar', ['baz' => 'bar']),
192+
new ServiceTag('foo.baz', ['baz' => 'baz']),
193+
]),
194+
]),
195+
],
196+
],
197+
[
198+
'services' => new DefaultServiceMap([
199+
new Service('Foo', 'Foo', true, false, null, [
200+
new ServiceTag('foo.bar', ['baz' => 'bar']),
201+
]),
202+
]),
203+
],
204+
];
205+
206+
yield 'new service added' => [
207+
[
208+
[
209+
'services' => new DefaultServiceMap([
210+
new Service('Foo', 'Foo', true, false, null),
211+
]),
212+
],
213+
],
214+
[
215+
'services' => new DefaultServiceMap([
216+
new Service('Foo', 'Foo', true, false, null),
217+
new Service('Bar', 'Bar', true, false, null),
218+
]),
219+
],
220+
];
221+
222+
yield 'service removed' => [
223+
[
224+
[
225+
'services' => new DefaultServiceMap([
226+
new Service('Foo', 'Foo', true, false, null),
227+
new Service('Bar', 'Bar', true, false, null),
228+
]),
229+
],
230+
],
231+
[
232+
'services' => new DefaultServiceMap([
233+
new Service('Foo', 'Foo', true, false, null),
234+
]),
235+
],
236+
];
237+
238+
yield 'parameter value changes' => [
239+
[
240+
[
241+
'parameters' => new DefaultParameterMap([
242+
new Parameter('foo', 'foo'),
243+
new Parameter('bar', 'bar'),
244+
]),
245+
],
246+
[
247+
'parameters' => new DefaultParameterMap([
248+
new Parameter('bar', 'bar'),
249+
new Parameter('foo', 'foo'),
250+
]),
251+
],
252+
],
253+
[
254+
'parameters' => new DefaultParameterMap([
255+
new Parameter('foo', 'foo'),
256+
new Parameter('bar', 'buzz'),
257+
]),
258+
],
259+
];
260+
261+
yield 'new parameter added' => [
262+
[
263+
[
264+
'parameters' => new DefaultParameterMap([
265+
new Parameter('foo', 'foo'),
266+
]),
267+
],
268+
],
269+
[
270+
'parameters' => new DefaultParameterMap([
271+
new Parameter('foo', 'foo'),
272+
new Parameter('bar', 'bar'),
273+
]),
274+
],
275+
];
276+
277+
yield 'parameter removed' => [
278+
[
279+
[
280+
'parameters' => new DefaultParameterMap([
281+
new Parameter('foo', 'foo'),
282+
new Parameter('bar', 'bar'),
283+
]),
284+
],
285+
],
286+
[
287+
'parameters' => new DefaultParameterMap([
288+
new Parameter('foo', 'foo'),
289+
]),
290+
],
291+
];
292+
}
293+
294+
}

0 commit comments

Comments
 (0)
Please sign in to comment.