Skip to content

Commit 8b7d263

Browse files
committed
Register services from DI attributes found in configured directories
PHPStan's dependency injection attributes were only usable from phpstan-src itself. They are collected at composer install time into vendor/attributes.php, which only ever covers phpstan-src's own src directory, and inside the PHAR the collector lives under a build-specific namespace prefix. Extensions shipped as separate packages therefore had no way in, and had to list every rule, type extension and collector by hand in a services: section. Add an autowiredServiceDirectories parameter. Each configuration file contributes its own directories, and because the parameter is a list the entries merge rather than overwrite, so one extension adding to it cannot hide another's classes. AutowiredServiceDiscoverer scans those directories, using the bundled parser only as a filter for classes carrying a known attribute, then reflects on the survivors to build the same TargetClass and TargetMethodParameter objects the collector produces. The compiler extensions merge both sources, so discovered classes go through the existing registration paths unchanged, including the auto-tagging derived from #[ExtensionInterface]. The scanned files feed the container cache key - otherwise editing a discovered rule would keep being served from the cached container. Parsing is deferred to the first attribute lookup, so a container that never asks does no work beyond listing the files, and the pre-filter matches the namespace the attributes live in rather than their names, which is both a single needle per file and immune to grouped or aliased use statements. getInterfaceTagMapping() now takes the ContainerBuilder, since the mapping depends on the configured directories and two containers in one process can disagree about them. Hoist its result out of the loop over autowired services in AutowiredAttributeServicesExtension: it used to be a free static memo, and calling it per service would otherwise re-derive the discoverer several hundred times per compile. #[ContainerExtension] is rejected with an explicit message. Nette snapshots its extension list before any of this runs, so compiler extensions still need an extensions: section. Mark the attributes extensions are now expected to reach for as @api.
1 parent 65fd609 commit 8b7d263

33 files changed

Lines changed: 954 additions & 35 deletions

‎build/baseline-pre-8.0.neon‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ parameters:
2424
count: 2
2525
path: PHPStan/Build/TurboAttributeCollector.php
2626

27+
-
28+
rawMessage: 'Call to an undefined method ReflectionClass<object>::getAttributes().'
29+
identifier: method.notFound
30+
count: 1
31+
path: ../src/DependencyInjection/AutowiredServiceDiscoverer.php
32+
33+
-
34+
rawMessage: 'Call to an undefined method ReflectionParameter::getAttributes().'
35+
identifier: method.notFound
36+
count: 1
37+
path: ../src/DependencyInjection/AutowiredServiceDiscoverer.php
38+
2739
-
2840
rawMessage: 'Call to an undefined method ReflectionClass::isReadOnly().'
2941
identifier: method.notFound

‎conf/config.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ parameters:
103103
mixinExcludeClasses: []
104104
scanFiles: []
105105
scanDirectories: []
106+
autowiredServiceDirectories: []
106107
parallel:
107108
jobSize: 20
108109
processTimeout: 600.0

‎conf/parametersSchema.neon‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ parametersSchema:
177177
mixinExcludeClasses: listOf(string())
178178
scanFiles: listOf(string())
179179
scanDirectories: listOf(string())
180+
autowiredServiceDirectories: listOf(string())
180181
editorUrl: schema(string(), nullable())
181182
editorUrlTitle: schema(string(), nullable())
182183
errorFormat: schema(string(), nullable())
@@ -224,6 +225,7 @@ expandRelativePaths:
224225
- '[parameters][bootstrapFiles][]'
225226
- '[parameters][scanFiles][]'
226227
- '[parameters][scanDirectories][]'
228+
- '[parameters][autowiredServiceDirectories][]'
227229
- '[parameters][tmpDir]'
228230
- '[parameters][pro][tmpDir]'
229231
- '[parameters][memoryLimitFile]'

‎phpstan-baseline.neon‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ parameters:
213213
-
214214
rawMessage: 'Call to static method expand() of internal class Nette\DI\Helpers from outside its root namespace Nette.'
215215
identifier: staticMethod.internalClass
216-
count: 1
216+
count: 2
217217
path: src/DependencyInjection/ContainerFactory.php
218218

219219
-

‎src/DependencyInjection/AutowiredAttributeServicesExtension.php‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use ReflectionClass;
2323
use stdClass;
2424
use function array_key_exists;
25+
use function array_merge;
2526
use function array_slice;
2627
use function count;
2728
use function explode;
@@ -51,8 +52,12 @@ public function loadConfiguration(): void
5152
{
5253
require_once __DIR__ . '/../../vendor/attributes.php';
5354
$builder = $this->getContainerBuilder();
55+
$discoverer = AutowiredServiceDiscoverer::createFromContainerBuilder($builder);
5456

55-
$autowiredParameters = Attributes::findTargetMethodParameters(AutowiredParameter::class);
57+
$autowiredParameters = array_merge(
58+
Attributes::findTargetMethodParameters(AutowiredParameter::class),
59+
$discoverer->findTargetMethodParameters(AutowiredParameter::class),
60+
);
5661
$constructorParameters = [];
5762
foreach ($autowiredParameters as $parameter) {
5863
if (strcasecmp($parameter->method, '__construct') !== 0) {
@@ -63,7 +68,9 @@ public function loadConfiguration(): void
6368
$constructorParameters[$lowerClass][] = $parameter;
6469
}
6570

66-
foreach (Attributes::findTargetClasses(AutowiredService::class) as $class) {
71+
$interfaceTagMapping = ValidateServiceTagsExtension::getInterfaceTagMapping($builder);
72+
73+
foreach (array_merge(Attributes::findTargetClasses(AutowiredService::class), $discoverer->findTargetClasses(AutowiredService::class)) as $class) {
6774
$reflection = new ReflectionClass($class->name);
6875
$attribute = $class->attribute;
6976

@@ -82,7 +89,7 @@ public function loadConfiguration(): void
8289
continue;
8390
}
8491

85-
foreach (ValidateServiceTagsExtension::getInterfaceTagMapping() as $interface => $tag) {
92+
foreach ($interfaceTagMapping as $interface => $tag) {
8693
if (!$reflection->implementsInterface($interface)) {
8794
continue;
8895
}
@@ -91,7 +98,7 @@ public function loadConfiguration(): void
9198
}
9299
}
93100

94-
foreach (Attributes::findTargetClasses(NonAutowiredService::class) as $class) {
101+
foreach (array_merge(Attributes::findTargetClasses(NonAutowiredService::class), $discoverer->findTargetClasses(NonAutowiredService::class)) as $class) {
95102
$attribute = $class->attribute;
96103

97104
$definition = $builder->addDefinition($attribute->name)
@@ -106,7 +113,7 @@ public function loadConfiguration(): void
106113
self::processConstructorParameters($builder, $class->name, $definition, $constructorParameters);
107114
}
108115

109-
foreach (Attributes::findTargetClasses(GenerateFactory::class) as $class) {
116+
foreach (array_merge(Attributes::findTargetClasses(GenerateFactory::class), $discoverer->findTargetClasses(GenerateFactory::class)) as $class) {
110117
$attribute = $class->attribute;
111118
$definition = $builder->addFactoryDefinition(null)
112119
->setImplement($attribute->interface);
@@ -125,7 +132,7 @@ public function loadConfiguration(): void
125132
return;
126133
}
127134

128-
foreach (Attributes::findTargetClasses(RegisteredRule::class) as $class) {
135+
foreach (array_merge(Attributes::findTargetClasses(RegisteredRule::class), $discoverer->findTargetClasses(RegisteredRule::class)) as $class) {
129136
$attribute = $class->attribute;
130137
if ($attribute->level > $config->level) {
131138
continue;
@@ -139,7 +146,7 @@ public function loadConfiguration(): void
139146
self::processConstructorParameters($builder, $class->name, $definition, $constructorParameters);
140147
}
141148

142-
foreach (Attributes::findTargetClasses(RegisteredCollector::class) as $class) {
149+
foreach (array_merge(Attributes::findTargetClasses(RegisteredCollector::class), $discoverer->findTargetClasses(RegisteredCollector::class)) as $class) {
143150
$attribute = $class->attribute;
144151
if ($attribute->level > $config->level) {
145152
continue;

‎src/DependencyInjection/AutowiredExtensions.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
*
1313
* Works thanks to https://github.com/ondrejmirtes/composer-attribute-collector
1414
* and AutowiredExtensionsExtension.
15+
*
16+
* Extensions distributed outside phpstan-src list the directories to look for
17+
* this attribute in through the `autowiredServiceDirectories` parameter.
18+
*
19+
* @api
1520
*/
1621
#[Attribute(flags: Attribute::TARGET_PARAMETER)]
1722
final class AutowiredExtensions

‎src/DependencyInjection/AutowiredExtensionsExtension.php‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use ReflectionMethod;
1313
use ReflectionNamedType;
1414
use function array_key_exists;
15+
use function array_merge;
1516
use function is_string;
1617
use function sprintf;
1718
use function str_replace;
@@ -40,7 +41,7 @@ public static function getCollectionServiceName(string $extensionInterfaceName):
4041
public function loadConfiguration(): void
4142
{
4243
$builder = $this->getContainerBuilder();
43-
foreach (ValidateServiceTagsExtension::getInterfaceTagMapping() as $interface => $tag) {
44+
foreach (ValidateServiceTagsExtension::getInterfaceTagMapping($builder) as $interface => $tag) {
4445
$builder->addDefinition(self::getCollectionServiceName($interface))
4546
->setType(LazyExtensionsCollection::class)
4647
->setArgument('tagName', $tag)
@@ -53,10 +54,14 @@ public function beforeCompile(): void
5354
{
5455
require_once __DIR__ . '/../../vendor/attributes.php';
5556
$builder = $this->getContainerBuilder();
56-
$mapping = ValidateServiceTagsExtension::getInterfaceTagMapping();
57+
$mapping = ValidateServiceTagsExtension::getInterfaceTagMapping($builder);
5758

5859
$parametersByClass = [];
59-
foreach (Attributes::findTargetMethodParameters(AutowiredExtensions::class) as $parameter) {
60+
$autowiredExtensions = array_merge(
61+
Attributes::findTargetMethodParameters(AutowiredExtensions::class),
62+
AutowiredServiceDiscoverer::createFromContainerBuilder($builder)->findTargetMethodParameters(AutowiredExtensions::class),
63+
);
64+
foreach ($autowiredExtensions as $parameter) {
6065
if (strcasecmp($parameter->method, '__construct') !== 0) {
6166
throw new ShouldNotHappenException(sprintf('Attribute #[AutowiredExtensions] is only supported on constructor parameters, found on %s::%s() $%s.', $parameter->class, $parameter->method, $parameter->name));
6267
}

‎src/DependencyInjection/AutowiredParameter.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
*
1313
* Works thanks to https://github.com/ondrejmirtes/composer-attribute-collector
1414
* and AutowiredAttributeServicesExtension.
15+
*
16+
* Extensions distributed outside phpstan-src list the directories to look for
17+
* this attribute in through the `autowiredServiceDirectories` parameter.
18+
*
19+
* @api
1520
*/
1621
#[Attribute(flags: Attribute::TARGET_PARAMETER)]
1722
final class AutowiredParameter

‎src/DependencyInjection/AutowiredService.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
*
1414
* Works thanks to https://github.com/ondrejmirtes/composer-attribute-collector
1515
* and AutowiredAttributeServicesExtension.
16+
*
17+
* Extensions distributed outside phpstan-src list the directories to look for
18+
* this attribute in through the `autowiredServiceDirectories` parameter.
19+
*
20+
* @api
1621
*/
1722
#[Attribute(flags: Attribute::TARGET_CLASS)]
1823
final class AutowiredService

0 commit comments

Comments
 (0)