Skip to content

Commit fdd0cb5

Browse files
xificurkclaude
andauthored
Improve laziness of container XML parsing (#487)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a02230d commit fdd0cb5

6 files changed

Lines changed: 208 additions & 5 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,14 @@ The extension auto-registers via `composer.json` `extra.phpstan.includes` when u
139139

140140
### Service Map
141141

142-
The extension reads Symfony's compiled container XML dump to build a map of services. This enables detection of unknown/private services and correct return types for `get()` calls. The `ServiceMap` interface has two implementations:
143-
- `DefaultServiceMap` - populated from XML parsing
142+
The extension reads Symfony's compiled container XML dump to build a map of services. This enables detection of unknown/private services and correct return types for `get()` calls. The `ServiceMap` interface has three implementations:
143+
- `DefaultServiceMap` - populated from XML parsing via `XmlServiceMapFactory`
144144
- `FakeServiceMap` - no-op fallback when no container XML is configured
145+
- `LazyServiceMap` - lazy wrapper injected by the DI container; defers XML parsing until first access
145146

146147
### Parameter Map
147148

148-
Similar to ServiceMap, reads container parameters from the XML dump for type-aware `getParameter()` return types.
149+
Similar to ServiceMap, reads container parameters from the XML dump for type-aware `getParameter()` return types. Has three implementations: `DefaultParameterMap` (from XML), `FakeParameterMap` (no-op fallback), and `LazyParameterMap` (lazy wrapper, same pattern as `LazyServiceMap`).
149150

150151
### Console Application Resolver
151152

extension.neon

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ services:
4848
arguments:
4949
containerXmlPath: %symfony.containerXmlPath%
5050
-
51-
factory: @symfony.serviceMapFactory::create()
51+
class: PHPStan\Symfony\ServiceMap
52+
factory: PHPStan\Symfony\LazyServiceMap::create
5253

5354
# parameter map
5455
symfony.parameterMapFactory:
@@ -57,7 +58,8 @@ services:
5758
arguments:
5859
containerXmlPath: %symfony.containerXmlPath%
5960
-
60-
factory: @symfony.parameterMapFactory::create()
61+
class: PHPStan\Symfony\ParameterMap
62+
factory: PHPStan\Symfony\LazyParameterMap::create
6163

6264
# message map
6365
symfony.messageMapFactory:

src/Symfony/LazyParameterMap.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PhpParser\Node\Expr;
6+
use PHPStan\Analyser\Scope;
7+
8+
abstract class LazyParameterMap implements ParameterMap
9+
{
10+
11+
private function __construct()
12+
{
13+
}
14+
15+
final public static function create(ParameterMapFactory $parameterMapFactory): self
16+
{
17+
// Workaround to make the static method getParameterKeysFromNode() work without sharing state across all LazyParameterMap instances
18+
$lazyParameterMap = new class () extends LazyParameterMap
19+
{
20+
21+
protected static ParameterMapFactory $parameterMapFactory;
22+
23+
protected static ?ParameterMap $parameterMap;
24+
25+
protected static function getParameterMap(): ParameterMap
26+
{
27+
self::$parameterMap ??= self::$parameterMapFactory->create();
28+
return self::$parameterMap;
29+
}
30+
31+
};
32+
$lazyParameterMap::$parameterMapFactory = $parameterMapFactory;
33+
$lazyParameterMap::$parameterMap = null;
34+
35+
return $lazyParameterMap;
36+
}
37+
38+
abstract protected static function getParameterMap(): ParameterMap;
39+
40+
/**
41+
* @return ParameterDefinition[]
42+
*/
43+
public function getParameters(): array
44+
{
45+
return static::getParameterMap()->getParameters();
46+
}
47+
48+
public function getParameter(string $key): ?ParameterDefinition
49+
{
50+
return static::getParameterMap()->getParameter($key);
51+
}
52+
53+
public static function getParameterKeysFromNode(Expr $node, Scope $scope): array
54+
{
55+
return static::getParameterMap()::getParameterKeysFromNode($node, $scope);
56+
}
57+
58+
}

src/Symfony/LazyServiceMap.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PhpParser\Node\Expr;
6+
use PHPStan\Analyser\Scope;
7+
8+
abstract class LazyServiceMap implements ServiceMap
9+
{
10+
11+
private function __construct()
12+
{
13+
}
14+
15+
final public static function create(ServiceMapFactory $serviceMapFactory): self
16+
{
17+
// Workaround to make the static method getServiceIdFromNode() work without sharing state across all LazyServiceMap instances
18+
$lazyServiceMap = new class () extends LazyServiceMap
19+
{
20+
21+
public static ServiceMapFactory $serviceMapFactory;
22+
23+
public static ?ServiceMap $serviceMap;
24+
25+
protected static function getServiceMap(): ServiceMap
26+
{
27+
self::$serviceMap ??= self::$serviceMapFactory->create();
28+
return self::$serviceMap;
29+
}
30+
31+
};
32+
$lazyServiceMap::$serviceMapFactory = $serviceMapFactory;
33+
$lazyServiceMap::$serviceMap = null;
34+
35+
return $lazyServiceMap;
36+
}
37+
38+
abstract protected static function getServiceMap(): ServiceMap;
39+
40+
/**
41+
* @return ServiceDefinition[]
42+
*/
43+
public function getServices(): array
44+
{
45+
return static::getServiceMap()->getServices();
46+
}
47+
48+
public function getService(string $id): ?ServiceDefinition
49+
{
50+
return static::getServiceMap()->getService($id);
51+
}
52+
53+
public static function getServiceIdFromNode(Expr $node, Scope $scope): ?string
54+
{
55+
return static::getServiceMap()::getServiceIdFromNode($node, $scope);
56+
}
57+
58+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PhpParser\Node\Expr\Variable;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Type\Constant\ConstantStringType;
8+
use PHPUnit\Framework\TestCase;
9+
10+
final class LazyParameterMapTest extends TestCase
11+
{
12+
13+
public function testFactoryIsNotCalledOnConstruction(): void
14+
{
15+
$factory = $this->createMock(ParameterMapFactory::class);
16+
$factory->expects(self::never())->method('create');
17+
18+
LazyParameterMap::create($factory);
19+
}
20+
21+
public function testDelegation(): void
22+
{
23+
$parameter = new Parameter('app.string', 'abcdef');
24+
$innerMap = new DefaultParameterMap(['app.string' => $parameter]);
25+
26+
$factory = $this->createMock(ParameterMapFactory::class);
27+
$factory->expects(self::once())->method('create')->willReturn($innerMap);
28+
29+
$lazyMap = LazyParameterMap::create($factory);
30+
31+
self::assertSame($innerMap->getParameters(), $lazyMap->getParameters());
32+
self::assertSame($innerMap->getParameter('app.string'), $lazyMap->getParameter('app.string'));
33+
self::assertNull($lazyMap->getParameter('unknown'));
34+
35+
$node = new Variable('x');
36+
$scope = $this->createMock(Scope::class);
37+
$scope->method('getType')->with($node)->willReturn(new ConstantStringType('app.string'));
38+
39+
self::assertSame($innerMap::getParameterKeysFromNode($node, $scope), $lazyMap::getParameterKeysFromNode($node, $scope));
40+
}
41+
42+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PhpParser\Node\Expr\Variable;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Type\Constant\ConstantStringType;
8+
use PHPUnit\Framework\TestCase;
9+
10+
final class LazyServiceMapTest extends TestCase
11+
{
12+
13+
public function testFactoryIsNotCalledOnConstruction(): void
14+
{
15+
$factory = $this->createMock(ServiceMapFactory::class);
16+
$factory->expects(self::never())->method('create');
17+
18+
LazyServiceMap::create($factory);
19+
}
20+
21+
public function testDelegation(): void
22+
{
23+
$service = new Service('withClass', 'Foo', false, false, null);
24+
$innerMap = new DefaultServiceMap(['withClass' => $service]);
25+
26+
$factory = $this->createMock(ServiceMapFactory::class);
27+
$factory->expects(self::once())->method('create')->willReturn($innerMap);
28+
29+
$lazyMap = LazyServiceMap::create($factory);
30+
31+
self::assertSame($innerMap->getServices(), $lazyMap->getServices());
32+
self::assertSame($innerMap->getService('withClass'), $lazyMap->getService('withClass'));
33+
self::assertNull($lazyMap->getService('unknown'));
34+
35+
$node = new Variable('x');
36+
$scope = $this->createMock(Scope::class);
37+
$scope->method('getType')->with($node)->willReturn(new ConstantStringType('withClass'));
38+
39+
self::assertSame($innerMap::getServiceIdFromNode($node, $scope), $lazyMap::getServiceIdFromNode($node, $scope));
40+
}
41+
42+
}

0 commit comments

Comments
 (0)