From 36d8b3e5bc12ca88ab6e6f9ef5575afc31cb601a Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 4 Dec 2024 13:49:08 +0100 Subject: [PATCH 01/24] [DependencyInjection] Make `#[AsTaggedItem]` repeatable --- Attribute/AsTaggedItem.php | 2 +- CHANGELOG.md | 5 +++ Compiler/PriorityTaggedServiceTrait.php | 15 ++++++++ .../PriorityTaggedServiceTraitTest.php | 34 +++++++++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/Attribute/AsTaggedItem.php b/Attribute/AsTaggedItem.php index 2e649bdea..cc3306c73 100644 --- a/Attribute/AsTaggedItem.php +++ b/Attribute/AsTaggedItem.php @@ -16,7 +16,7 @@ * * @author Nicolas Grekas */ -#[\Attribute(\Attribute::TARGET_CLASS)] +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] class AsTaggedItem { /** diff --git a/CHANGELOG.md b/CHANGELOG.md index 4287747ec..9d7334a6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.3 +--- + + * Make `#[AsTaggedItem]` repeatable + 7.2 --- diff --git a/Compiler/PriorityTaggedServiceTrait.php b/Compiler/PriorityTaggedServiceTrait.php index 77a1d7ef8..9f443256a 100644 --- a/Compiler/PriorityTaggedServiceTrait.php +++ b/Compiler/PriorityTaggedServiceTrait.php @@ -92,6 +92,21 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam $services[] = [$priority, ++$i, $index, $serviceId, $class]; } + + if ($class) { + $attributes = (new \ReflectionClass($class))->getAttributes(AsTaggedItem::class); + $attributeCount = \count($attributes); + + foreach ($attributes as $attribute) { + $instance = $attribute->newInstance(); + + if (!$instance->index && 1 < $attributeCount) { + throw new InvalidArgumentException(\sprintf('Attribute "%s" on class "%s" cannot have an empty index when repeated.', AsTaggedItem::class, $class)); + } + + $services[] = [$instance->priority ?? 0, ++$i, $instance->index ?? $serviceId, $serviceId, $class]; + } + } } uasort($services, static fn ($a, $b) => $b[0] <=> $a[0] ?: $a[1] <=> $b[1]); diff --git a/Tests/Compiler/PriorityTaggedServiceTraitTest.php b/Tests/Compiler/PriorityTaggedServiceTraitTest.php index aac1a2e1a..3f767257d 100644 --- a/Tests/Compiler/PriorityTaggedServiceTraitTest.php +++ b/Tests/Compiler/PriorityTaggedServiceTraitTest.php @@ -218,6 +218,9 @@ public function testTaggedItemAttributes() $container->register('service5', HelloNamedService2::class) ->setAutoconfigured(true) ->addTag('my_custom_tag'); + $container->register('service6', MultiTagHelloNamedService::class) + ->setAutoconfigured(true) + ->addTag('my_custom_tag'); (new ResolveInstanceofConditionalsPass())->process($container); @@ -226,14 +229,33 @@ public function testTaggedItemAttributes() $tag = new TaggedIteratorArgument('my_custom_tag', 'foo', 'getFooBar', exclude: ['service4', 'service5']); $expected = [ 'service3' => new TypedReference('service3', HelloNamedService2::class), + 'multi_hello_2' => new TypedReference('service6', MultiTagHelloNamedService::class), 'hello' => new TypedReference('service2', HelloNamedService::class), + 'multi_hello_1' => new TypedReference('service6', MultiTagHelloNamedService::class), 'service1' => new TypedReference('service1', FooTagClass::class), ]; + $services = $priorityTaggedServiceTraitImplementation->test($tag, $container); $this->assertSame(array_keys($expected), array_keys($services)); $this->assertEquals($expected, $priorityTaggedServiceTraitImplementation->test($tag, $container)); } + public function testTaggedItemAttributesRepeatedWithoutNameThrows() + { + $container = new ContainerBuilder(); + $container->register('service1', MultiNoNameTagHelloNamedService::class) + ->setAutoconfigured(true) + ->addTag('my_custom_tag'); + + (new ResolveInstanceofConditionalsPass())->process($container); + $tag = new TaggedIteratorArgument('my_custom_tag', 'foo', 'getFooBar', exclude: ['service4', 'service5']); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Attribute "Symfony\Component\DependencyInjection\Attribute\AsTaggedItem" on class "Symfony\Component\DependencyInjection\Tests\Compiler\MultiNoNameTagHelloNamedService" cannot have an empty index when repeated.'); + + (new PriorityTaggedServiceTraitImplementation())->test($tag, $container); + } + public function testResolveIndexedTags() { $container = new ContainerBuilder(); @@ -283,6 +305,18 @@ class HelloNamedService2 { } +#[AsTaggedItem(index: 'multi_hello_1', priority: 1)] +#[AsTaggedItem(index: 'multi_hello_2', priority: 2)] +class MultiTagHelloNamedService +{ +} + +#[AsTaggedItem(priority: 1)] +#[AsTaggedItem(priority: 2)] +class MultiNoNameTagHelloNamedService +{ +} + interface HelloInterface { public static function getFooBar(): string; From 09a04f7055c94e91750643905db0d06229524e6f Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Wed, 11 Dec 2024 14:08:35 +0100 Subject: [PATCH 02/24] chore: PHP CS Fixer fixes --- Compiler/ResolveBindingsPass.php | 2 +- Tests/Compiler/ResolveAutowireInlineAttributesPassTest.php | 1 - Tests/Loader/XmlFileLoaderTest.php | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Compiler/ResolveBindingsPass.php b/Compiler/ResolveBindingsPass.php index 4fca2081b..b2c6f6ef7 100644 --- a/Compiler/ResolveBindingsPass.php +++ b/Compiler/ResolveBindingsPass.php @@ -228,7 +228,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed foreach ($names as $key => $name) { if (\array_key_exists($name, $arguments) && (0 === $key || \array_key_exists($key - 1, $arguments))) { - if (!array_key_exists($key, $arguments)) { + if (!\array_key_exists($key, $arguments)) { $arguments[$key] = $arguments[$name]; } unset($arguments[$name]); diff --git a/Tests/Compiler/ResolveAutowireInlineAttributesPassTest.php b/Tests/Compiler/ResolveAutowireInlineAttributesPassTest.php index 58cb1cd38..a0d1ec50f 100644 --- a/Tests/Compiler/ResolveAutowireInlineAttributesPassTest.php +++ b/Tests/Compiler/ResolveAutowireInlineAttributesPassTest.php @@ -18,7 +18,6 @@ use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass; use Symfony\Component\DependencyInjection\Compiler\ResolveNamedArgumentsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php'; diff --git a/Tests/Loader/XmlFileLoaderTest.php b/Tests/Loader/XmlFileLoaderTest.php index 1b4361411..f962fa106 100644 --- a/Tests/Loader/XmlFileLoaderTest.php +++ b/Tests/Loader/XmlFileLoaderTest.php @@ -1280,7 +1280,7 @@ public function testStaticConstructor() public function testStaticConstructorWithFactoryThrows() { $container = new ContainerBuilder(); - $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath . '/xml')); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); $this->expectException(LogicException::class); $this->expectExceptionMessage('The "static_constructor" service cannot declare a factory as well as a constructor.'); @@ -1341,7 +1341,7 @@ public function testUnknownConstantAsKey() public function testDeprecatedTagged() { $container = new ContainerBuilder(); - $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath . '/xml')); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); $this->expectUserDeprecationMessage(\sprintf('Since symfony/dependency-injection 7.2: Type "tagged" is deprecated for tag , use "tagged_iterator" instead in "%s/xml%sservices_with_deprecated_tagged.xml".', self::$fixturesPath, \DIRECTORY_SEPARATOR)); From c8952a4db77a96045027c56d65b25fb6dd3603db Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 2 Jan 2025 23:17:07 +0100 Subject: [PATCH 03/24] reuse the reflector tracked by the container builder --- Compiler/PriorityTaggedServiceTrait.php | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Compiler/PriorityTaggedServiceTrait.php b/Compiler/PriorityTaggedServiceTrait.php index 9f443256a..4befef860 100644 --- a/Compiler/PriorityTaggedServiceTrait.php +++ b/Compiler/PriorityTaggedServiceTrait.php @@ -64,6 +64,7 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam $definition = $container->getDefinition($serviceId); $class = $definition->getClass(); $class = $container->getParameterBag()->resolveValue($class) ?: null; + $reflector = null !== $class ? $container->getReflectionClass($class) : null; $checkTaggedItem = !$definition->hasTag($definition->isAutoconfigured() ? 'container.ignore_attributes' : $tagName); foreach ($attributes as $attribute) { @@ -71,8 +72,8 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam if (isset($attribute['priority'])) { $priority = $attribute['priority']; - } elseif (null === $defaultPriority && $defaultPriorityMethod && $class) { - $defaultPriority = PriorityTaggedServiceUtil::getDefault($container, $serviceId, $class, $defaultPriorityMethod, $tagName, 'priority', $checkTaggedItem); + } elseif (null === $defaultPriority && $defaultPriorityMethod && $reflector) { + $defaultPriority = PriorityTaggedServiceUtil::getDefault($serviceId, $reflector, $defaultPriorityMethod, $tagName, 'priority', $checkTaggedItem); } $priority ??= $defaultPriority ??= 0; @@ -84,8 +85,8 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam if (null !== $indexAttribute && isset($attribute[$indexAttribute])) { $index = $parameterBag->resolveValue($attribute[$indexAttribute]); } - if (null === $index && null === $defaultIndex && $defaultPriorityMethod && $class) { - $defaultIndex = PriorityTaggedServiceUtil::getDefault($container, $serviceId, $class, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute, $checkTaggedItem); + if (null === $index && null === $defaultIndex && $defaultPriorityMethod && $reflector) { + $defaultIndex = PriorityTaggedServiceUtil::getDefault($serviceId, $reflector, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute, $checkTaggedItem); } $decorated = $definition->getTag('container.decorator')[0]['id'] ?? null; $index = $index ?? $defaultIndex ?? $defaultIndex = $decorated ?? $serviceId; @@ -93,8 +94,8 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam $services[] = [$priority, ++$i, $index, $serviceId, $class]; } - if ($class) { - $attributes = (new \ReflectionClass($class))->getAttributes(AsTaggedItem::class); + if ($reflector) { + $attributes = $reflector->getAttributes(AsTaggedItem::class); $attributeCount = \count($attributes); foreach ($attributes as $attribute) { @@ -137,9 +138,11 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam */ class PriorityTaggedServiceUtil { - public static function getDefault(ContainerBuilder $container, string $serviceId, string $class, string $defaultMethod, string $tagName, ?string $indexAttribute, bool $checkTaggedItem): string|int|null + public static function getDefault(string $serviceId, \ReflectionClass $r, string $defaultMethod, string $tagName, ?string $indexAttribute, bool $checkTaggedItem): string|int|null { - if (!($r = $container->getReflectionClass($class)) || (!$checkTaggedItem && !$r->hasMethod($defaultMethod))) { + $class = $r->getName(); + + if (!$checkTaggedItem && !$r->hasMethod($defaultMethod)) { return null; } From c3ad427e59f152d096db2914e0dc62eb735cf55c Mon Sep 17 00:00:00 2001 From: Karoly Negyesi Date: Wed, 18 Dec 2024 19:52:35 +0100 Subject: [PATCH 04/24] [DependencyInjection] Support @> as a shorthand for !service_closure in YamlFileLoader (Issue #59255) --- CHANGELOG.md | 1 + Loader/YamlFileLoader.php | 5 +++++ .../yaml/services_with_short_service_closure.yml | 8 ++++++++ Tests/Loader/YamlFileLoaderTest.php | 9 +++++++++ 4 files changed, 23 insertions(+) create mode 100644 Tests/Fixtures/yaml/services_with_short_service_closure.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d7334a6d..45b519623 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Make `#[AsTaggedItem]` repeatable + * Support `@>` as a shorthand for `!service_closure` in yaml files 7.2 --- diff --git a/Loader/YamlFileLoader.php b/Loader/YamlFileLoader.php index a4a93f63a..c3b1bf255 100644 --- a/Loader/YamlFileLoader.php +++ b/Loader/YamlFileLoader.php @@ -923,6 +923,11 @@ private function resolveServices(mixed $value, string $file, bool $isParameter = return new Expression(substr($value, 2)); } elseif (\is_string($value) && str_starts_with($value, '@')) { + if (str_starts_with($value, '@>')) { + $argument = $this->resolveServices(substr_replace($value, '', 1, 1), $file, $isParameter); + + return new ServiceClosureArgument($argument); + } if (str_starts_with($value, '@@')) { $value = substr($value, 1); $invalidBehavior = null; diff --git a/Tests/Fixtures/yaml/services_with_short_service_closure.yml b/Tests/Fixtures/yaml/services_with_short_service_closure.yml new file mode 100644 index 000000000..7215e538d --- /dev/null +++ b/Tests/Fixtures/yaml/services_with_short_service_closure.yml @@ -0,0 +1,8 @@ +services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + public: true + synthetic: true + foo: + class: Foo + arguments: ['@>bar'] diff --git a/Tests/Loader/YamlFileLoaderTest.php b/Tests/Loader/YamlFileLoaderTest.php index 8da59796e..97866064f 100644 --- a/Tests/Loader/YamlFileLoaderTest.php +++ b/Tests/Loader/YamlFileLoaderTest.php @@ -466,6 +466,15 @@ public function testParseServiceClosure() $this->assertEquals(new ServiceClosureArgument(new Reference('bar', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), $container->getDefinition('foo')->getArgument(0)); } + public function testParseShortServiceClosure() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services_with_short_service_closure.yml'); + + $this->assertEquals(new ServiceClosureArgument(new Reference('bar')), $container->getDefinition('foo')->getArgument(0)); + } + public function testNameOnlyTagsAreAllowedAsString() { $container = new ContainerBuilder(); From fc11cec4a3ec4be94421d83eadc6dc837cba73f0 Mon Sep 17 00:00:00 2001 From: kor3k Date: Sat, 21 Dec 2024 19:05:36 +0100 Subject: [PATCH 05/24] Sync Security\ExpressionLanguage constructor with parent change typehint array -> iterable --- ExpressionLanguage.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ExpressionLanguage.php b/ExpressionLanguage.php index 84d45dbdd..79de5b049 100644 --- a/ExpressionLanguage.php +++ b/ExpressionLanguage.php @@ -27,8 +27,12 @@ */ class ExpressionLanguage extends BaseExpressionLanguage { - public function __construct(?CacheItemPoolInterface $cache = null, array $providers = [], ?callable $serviceCompiler = null, ?\Closure $getEnv = null) + public function __construct(?CacheItemPoolInterface $cache = null, iterable $providers = [], ?callable $serviceCompiler = null, ?\Closure $getEnv = null) { + if (!\is_array($providers)) { + $providers = iterator_to_array($providers, false); + } + // prepend the default provider to let users override it easily array_unshift($providers, new ExpressionLanguageProvider($serviceCompiler, $getEnv)); From c42a20ccb1e3524def5d2c62b047ae53b41c9496 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Fri, 10 Jan 2025 15:17:09 +0100 Subject: [PATCH 06/24] chore: PHP CS Fixer fixes --- Tests/Argument/LazyClosureTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Tests/Argument/LazyClosureTest.php b/Tests/Argument/LazyClosureTest.php index 46ef15917..4a7b16a7e 100644 --- a/Tests/Argument/LazyClosureTest.php +++ b/Tests/Argument/LazyClosureTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\DependencyInjection\Tests\Argument; -use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\LazyClosure; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -23,7 +22,7 @@ public function testMagicGetThrows() { $closure = new LazyClosure(fn () => null); - $this->expectException(InvalidArgumentException::class); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Cannot read property "foo" from a lazy closure.'); $closure->foo; @@ -34,7 +33,7 @@ public function testThrowsWhenNotUsingInterface() $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Cannot create adapter for service "foo" because "Symfony\Component\DependencyInjection\Tests\Argument\LazyClosureTest" is not an interface.'); - LazyClosure::getCode('foo', [new \stdClass(), 'bar'], new Definition(LazyClosureTest::class), new ContainerBuilder(), 'foo'); + LazyClosure::getCode('foo', [new \stdClass(), 'bar'], new Definition(self::class), new ContainerBuilder(), 'foo'); } public function testThrowsOnNonFunctionalInterface() From bd9b43e8fbece27f9b7a33cd38a6c2e729d05e0e Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sat, 18 Jan 2025 17:45:23 +0100 Subject: [PATCH 07/24] chore: PHP CS Fixer fixes --- Tests/Argument/LazyClosureTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/Argument/LazyClosureTest.php b/Tests/Argument/LazyClosureTest.php index 4a7b16a7e..428227d19 100644 --- a/Tests/Argument/LazyClosureTest.php +++ b/Tests/Argument/LazyClosureTest.php @@ -61,5 +61,6 @@ public function foo(); interface NonFunctionalInterface { public function foo(); + public function bar(); } From b9cb37ab35c12d0c79783914ece8fe2afca4dcf4 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 6 Feb 2025 16:25:54 +0100 Subject: [PATCH 08/24] [DependencyInjection] Don't skip classes with private constructor when autodiscovering --- CHANGELOG.md | 1 + Compiler/AbstractRecursivePass.php | 2 +- Loader/FileLoader.php | 75 ++++++++++--------- Tests/Compiler/AutowirePassTest.php | 2 +- .../PrototypeStaticConstructor.php | 4 + Tests/Loader/FileLoaderTest.php | 24 ++++-- 6 files changed, 63 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45b519623..d24bb13c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Make `#[AsTaggedItem]` repeatable * Support `@>` as a shorthand for `!service_closure` in yaml files + * Don't skip classes with private constructor when autodiscovering 7.2 --- diff --git a/Compiler/AbstractRecursivePass.php b/Compiler/AbstractRecursivePass.php index dd8c9cd29..55f8ee7e9 100644 --- a/Compiler/AbstractRecursivePass.php +++ b/Compiler/AbstractRecursivePass.php @@ -181,7 +181,7 @@ protected function getConstructor(Definition $definition, bool $required): ?\Ref throw new RuntimeException(\sprintf('Invalid service "%s": class%s has no constructor.', $this->currentId, \sprintf($class !== $this->currentId ? ' "%s"' : '', $class))); } } elseif (!$r->isPublic()) { - throw new RuntimeException(\sprintf('Invalid service "%s": ', $this->currentId).\sprintf($class !== $this->currentId ? 'constructor of class "%s"' : 'its constructor', $class).' must be public.'); + throw new RuntimeException(\sprintf('Invalid service "%s": ', $this->currentId).\sprintf($class !== $this->currentId ? 'constructor of class "%s"' : 'its constructor', $class).' must be public. Did you miss configuring a factory or a static constructor? Try using the "#[Autoconfigure(constructor: ...)]" attribute for the latter.'); } return $r; diff --git a/Loader/FileLoader.php b/Loader/FileLoader.php index a4d812e37..9e17bc424 100644 --- a/Loader/FileLoader.php +++ b/Loader/FileLoader.php @@ -126,7 +126,7 @@ public function registerClasses(Definition $prototype, string $namespace, string $autoconfigureAttributes = new RegisterAutoconfigureAttributesPass(); $autoconfigureAttributes = $autoconfigureAttributes->accept($prototype) ? $autoconfigureAttributes : null; - $classes = $this->findClasses($namespace, $resource, (array) $exclude, $autoconfigureAttributes, $source); + $classes = $this->findClasses($namespace, $resource, (array) $exclude, $source); $getPrototype = static fn () => clone $prototype; $serialized = serialize($prototype); @@ -188,41 +188,46 @@ public function registerClasses(Definition $prototype, string $namespace, string } } - if (interface_exists($class, false)) { - $this->interfaces[] = $class; - } else { - $this->setDefinition($class, $definition = $getPrototype()); - if (null !== $errorMessage) { - $definition->addError($errorMessage); - - continue; + $r = null === $errorMessage ? $this->container->getReflectionClass($class) : null; + if ($r?->isAbstract() || $r?->isInterface()) { + if ($r->isInterface()) { + $this->interfaces[] = $class; } - $definition->setClass($class); + $autoconfigureAttributes?->processClass($this->container, $r); + continue; + } - $interfaces = []; - foreach (class_implements($class, false) as $interface) { - $this->singlyImplemented[$interface] = ($this->singlyImplemented[$interface] ?? $class) !== $class ? false : $class; - $interfaces[] = $interface; - } + $this->setDefinition($class, $definition = $getPrototype()); + if (null !== $errorMessage) { + $definition->addError($errorMessage); - if (!$autoconfigureAttributes) { - continue; + continue; + } + $definition->setClass($class); + + $interfaces = []; + foreach (class_implements($class, false) as $interface) { + $this->singlyImplemented[$interface] = ($this->singlyImplemented[$interface] ?? $class) !== $class ? false : $class; + $interfaces[] = $interface; + } + + if (!$autoconfigureAttributes) { + continue; + } + $r = $this->container->getReflectionClass($class); + $defaultAlias = 1 === \count($interfaces) ? $interfaces[0] : null; + foreach ($r->getAttributes(AsAlias::class) as $attr) { + /** @var AsAlias $attribute */ + $attribute = $attr->newInstance(); + $alias = $attribute->id ?? $defaultAlias; + $public = $attribute->public; + if (null === $alias) { + throw new LogicException(\sprintf('Alias cannot be automatically determined for class "%s". If you have used the #[AsAlias] attribute with a class implementing multiple interfaces, add the interface you want to alias to the first parameter of #[AsAlias].', $class)); } - $r = $this->container->getReflectionClass($class); - $defaultAlias = 1 === \count($interfaces) ? $interfaces[0] : null; - foreach ($r->getAttributes(AsAlias::class) as $attr) { - /** @var AsAlias $attribute */ - $attribute = $attr->newInstance(); - $alias = $attribute->id ?? $defaultAlias; - $public = $attribute->public; - if (null === $alias) { - throw new LogicException(\sprintf('Alias cannot be automatically determined for class "%s". If you have used the #[AsAlias] attribute with a class implementing multiple interfaces, add the interface you want to alias to the first parameter of #[AsAlias].', $class)); - } - if (isset($this->aliases[$alias])) { - throw new LogicException(\sprintf('The "%s" alias has already been defined with the #[AsAlias] attribute in "%s".', $alias, $this->aliases[$alias])); - } - $this->aliases[$alias] = new Alias($class, $public); + if (isset($this->aliases[$alias])) { + throw new LogicException(\sprintf('The "%s" alias has already been defined with the #[AsAlias] attribute in "%s".', $alias, $this->aliases[$alias])); } + $this->aliases[$alias] = new Alias($class, $public); } } @@ -304,7 +309,7 @@ protected function setDefinition(string $id, Definition $definition): void } } - private function findClasses(string $namespace, string $pattern, array $excludePatterns, ?RegisterAutoconfigureAttributesPass $autoconfigureAttributes, ?string $source): array + private function findClasses(string $namespace, string $pattern, array $excludePatterns, ?string $source): array { $parameterBag = $this->container->getParameterBag(); @@ -356,13 +361,9 @@ private function findClasses(string $namespace, string $pattern, array $excludeP throw new InvalidArgumentException(\sprintf('Expected to find class "%s" in file "%s" while importing services from resource "%s", but it was not found! Check the namespace prefix used with the resource.', $class, $path, $pattern)); } - if ($r->isInstantiable() || $r->isInterface()) { + if (!$r->isTrait()) { $classes[$class] = null; } - - if ($autoconfigureAttributes && !$r->isInstantiable()) { - $autoconfigureAttributes->processClass($this->container, $r); - } } // track only for new & removed files diff --git a/Tests/Compiler/AutowirePassTest.php b/Tests/Compiler/AutowirePassTest.php index f6aafdec9..114d514ad 100644 --- a/Tests/Compiler/AutowirePassTest.php +++ b/Tests/Compiler/AutowirePassTest.php @@ -174,7 +174,7 @@ public function testPrivateConstructorThrowsAutowireException() $pass->process($container); $this->fail('AutowirePass should have thrown an exception'); } catch (AutowiringFailedException $e) { - $this->assertSame('Invalid service "private_service": constructor of class "Symfony\Component\DependencyInjection\Tests\Compiler\PrivateConstructor" must be public.', (string) $e->getMessage()); + $this->assertSame('Invalid service "private_service": constructor of class "Symfony\Component\DependencyInjection\Tests\Compiler\PrivateConstructor" must be public. Did you miss configuring a factory or a static constructor? Try using the "#[Autoconfigure(constructor: ...)]" attribute for the latter.', (string) $e->getMessage()); } } diff --git a/Tests/Fixtures/Prototype/StaticConstructor/PrototypeStaticConstructor.php b/Tests/Fixtures/Prototype/StaticConstructor/PrototypeStaticConstructor.php index 87de94cb1..6ac5316a3 100644 --- a/Tests/Fixtures/Prototype/StaticConstructor/PrototypeStaticConstructor.php +++ b/Tests/Fixtures/Prototype/StaticConstructor/PrototypeStaticConstructor.php @@ -4,6 +4,10 @@ class PrototypeStaticConstructor implements PrototypeStaticConstructorInterface { + private function __construct() + { + } + public static function create(): static { return new self(); diff --git a/Tests/Loader/FileLoaderTest.php b/Tests/Loader/FileLoaderTest.php index a195f2a93..a43b4c5a1 100644 --- a/Tests/Loader/FileLoaderTest.php +++ b/Tests/Loader/FileLoaderTest.php @@ -33,6 +33,7 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\AnotherSub; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\AnotherSub\DeeperBaz; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\Baz; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\StaticConstructor\PrototypeStaticConstructor; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\BarInterface; use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\AliasBarInterface; @@ -380,11 +381,11 @@ public static function provideResourcesWithAsAliasAttributes(): iterable */ public function testRegisterClassesWithDuplicatedAsAlias(string $resource, string $expectedExceptionMessage) { - $this->expectException(LogicException::class); - $this->expectExceptionMessage($expectedExceptionMessage); - $container = new ContainerBuilder(); $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures')); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage($expectedExceptionMessage); $loader->registerClasses( (new Definition())->setAutoconfigured(true), 'Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\\', @@ -400,17 +401,28 @@ public static function provideResourcesWithDuplicatedAsAliasAttributes(): iterab public function testRegisterClassesWithAsAliasAndImplementingMultipleInterfaces() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Alias cannot be automatically determined for class "Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAliasMultipleInterface". If you have used the #[AsAlias] attribute with a class implementing multiple interfaces, add the interface you want to alias to the first parameter of #[AsAlias].'); - $container = new ContainerBuilder(); $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures')); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Alias cannot be automatically determined for class "Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAliasMultipleInterface". If you have used the #[AsAlias] attribute with a class implementing multiple interfaces, add the interface you want to alias to the first parameter of #[AsAlias].'); $loader->registerClasses( (new Definition())->setAutoconfigured(true), 'Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\\', 'PrototypeAsAlias/{WithAsAliasMultipleInterface,AliasBarInterface,AliasFooInterface}.php' ); } + + public function testRegisterClassesWithStaticConstructor() + { + $container = new ContainerBuilder(); + $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures')); + + $prototype = (new Definition())->setAutoconfigured(true); + $loader->registerClasses($prototype, 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\StaticConstructor\\', 'Prototype/StaticConstructor'); + + $this->assertTrue($container->has(PrototypeStaticConstructor::class)); + } } class TestFileLoader extends FileLoader From 7613403bf1b4b7f7633981bf925204d46e5e5542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 5 Feb 2025 14:11:25 +0100 Subject: [PATCH 09/24] [DependencyInjection] Add `Definition::addExcludedTag()` and `ContainerBuilder::findExcludedServiceIds()` for auto-discovering value-objects --- CHANGELOG.md | 2 ++ ContainerBuilder.php | 32 ++++++++++++++++++++ Definition.php | 14 +++++++++ Tests/ContainerBuilderTest.php | 55 ++++++++++++++++++++++++++++------ Tests/DefinitionTest.php | 10 +++++++ 5 files changed, 104 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d24bb13c9..0646d04bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ CHANGELOG * Make `#[AsTaggedItem]` repeatable * Support `@>` as a shorthand for `!service_closure` in yaml files * Don't skip classes with private constructor when autodiscovering + * Add `Definition::addExcludeTag()` and `ContainerBuilder::findExcludedServiceIds()` + for auto-configuration of classes excluded from the service container 7.2 --- diff --git a/ContainerBuilder.php b/ContainerBuilder.php index 7389ca631..f5270e31a 100644 --- a/ContainerBuilder.php +++ b/ContainerBuilder.php @@ -1351,6 +1351,38 @@ public function findTaggedServiceIds(string $name, bool $throwOnAbstract = false return $tags; } + /** + * Returns service ids for a given tag, asserting they have the "container.excluded" tag. + * + * Example: + * + * $container->register('foo')->addExcludeTag('my.tag', ['hello' => 'world']) + * + * $serviceIds = $container->findExcludedServiceIds('my.tag'); + * foreach ($serviceIds as $serviceId => $tags) { + * foreach ($tags as $tag) { + * echo $tag['hello']; + * } + * } + * + * @return array An array of tags with the tagged service as key, holding a list of attribute arrays + */ + public function findExcludedServiceIds(string $tagName): array + { + $this->usedTags[] = $tagName; + $tags = []; + foreach ($this->getDefinitions() as $id => $definition) { + if ($definition->hasTag($tagName)) { + if (!$definition->hasTag('container.excluded')) { + throw new InvalidArgumentException(\sprintf('The service "%s" tagged "%s" is missing the "container.excluded" tag.', $id, $tagName)); + } + $tags[$id] = $definition->getTag($tagName); + } + } + + return $tags; + } + /** * Returns all tags the defined services use. * diff --git a/Definition.php b/Definition.php index 0abdc5d56..682540e91 100644 --- a/Definition.php +++ b/Definition.php @@ -455,6 +455,20 @@ public function addTag(string $name, array $attributes = []): static return $this; } + /** + * Adds a tag to the definition and marks it as excluded. + * + * These definitions should be processed using {@see ContainerBuilder::findExcludedServiceIds()} + * + * @return $this + */ + public function addExcludeTag(string $name, array $attributes = []): static + { + return $this->addTag($name, $attributes) + ->addTag('container.excluded', ['source' => \sprintf('by tag "%s"', $name)]) + ->setAbstract(true); + } + /** * Whether this definition has a tag with the given name. */ diff --git a/Tests/ContainerBuilderTest.php b/Tests/ContainerBuilderTest.php index 544303bbe..882fffd19 100644 --- a/Tests/ContainerBuilderTest.php +++ b/Tests/ContainerBuilderTest.php @@ -1062,20 +1062,18 @@ public function testMergeLogicException() $container->merge(new ContainerBuilder()); } - public function testfindTaggedServiceIds() + public function testFindTaggedServiceIds() { $builder = new ContainerBuilder(); - $builder - ->register('foo', 'Bar\FooClass') + $builder->register('foo', 'Bar\FooClass') + ->setAbstract(true) ->addTag('foo', ['foo' => 'foo']) ->addTag('bar', ['bar' => 'bar']) - ->addTag('foo', ['foofoo' => 'foofoo']) - ; - $builder - ->register('bar', 'Bar\FooClass') + ->addTag('foo', ['foofoo' => 'foofoo']); + $builder->register('bar', 'Bar\FooClass') ->addTag('foo') - ->addTag('container.excluded') - ; + ->addTag('container.excluded'); + $this->assertEquals([ 'foo' => [ ['foo' => 'foo'], @@ -1085,6 +1083,45 @@ public function testfindTaggedServiceIds() $this->assertEquals([], $builder->findTaggedServiceIds('foobar'), '->findTaggedServiceIds() returns an empty array if there is annotated services'); } + public function testFindTaggedServiceIdsThrowsWhenAbstract() + { + $builder = new ContainerBuilder(); + $builder->register('foo', 'Bar\FooClass') + ->setAbstract(true) + ->addTag('foo', ['foo' => 'foo']); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The service "foo" tagged "foo" must not be abstract.'); + $builder->findTaggedServiceIds('foo', true); + } + + public function testFindExcludedServiceIds() + { + $builder = new ContainerBuilder(); + $builder->register('myservice', 'Bar\FooClass') + ->addTag('foo', ['foo' => 'foo']) + ->addTag('bar', ['bar' => 'bar']) + ->addTag('foo', ['foofoo' => 'foofoo']) + ->addExcludeTag('container.excluded'); + + $expected = ['myservice' => [['foo' => 'foo'], ['foofoo' => 'foofoo']]]; + $this->assertSame($expected, $builder->findExcludedServiceIds('foo')); + $this->assertSame([], $builder->findExcludedServiceIds('foofoo')); + } + + public function testFindExcludedServiceIdsThrowsWhenNotExcluded() + { + $builder = new ContainerBuilder(); + $builder->register('myservice', 'Bar\FooClass') + ->addTag('foo', ['foo' => 'foo']) + ->addTag('bar', ['bar' => 'bar']) + ->addTag('foo', ['foofoo' => 'foofoo']); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The service "myservice" tagged "foo" is missing the "container.excluded" tag.'); + $builder->findExcludedServiceIds('foo', true); + } + public function testFindUnusedTags() { $builder = new ContainerBuilder(); diff --git a/Tests/DefinitionTest.php b/Tests/DefinitionTest.php index 3a7c3a980..1a51c9af3 100644 --- a/Tests/DefinitionTest.php +++ b/Tests/DefinitionTest.php @@ -258,6 +258,16 @@ public function testTags() ], $def->getTags(), '->getTags() returns all tags'); } + public function testAddExcludeTag() + { + $def = new Definition('stdClass'); + $def->addExcludeTag('foo', ['bar' => true]); + + $this->assertSame([['bar' => true]], $def->getTag('foo')); + $this->assertTrue($def->isAbstract()); + $this->assertSame([['source' => 'by tag "foo"']], $def->getTag('container.excluded')); + } + public function testSetArgument() { $def = new Definition('stdClass'); From 30108d5dd20a5f8a5a08a3a90f1af72f840da92f Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 2 Mar 2025 16:03:52 +0100 Subject: [PATCH 10/24] replace assertEmpty() with stricter assertions --- Tests/Compiler/DecoratorServicePassTest.php | 4 ++-- Tests/Compiler/PassConfigTest.php | 10 +++++----- .../Compiler/ResolveInstanceofConditionalsPassTest.php | 8 ++++---- Tests/ContainerBuilderTest.php | 8 ++++---- Tests/Dumper/PhpDumperTest.php | 2 +- Tests/Loader/YamlFileLoaderTest.php | 2 +- Tests/ParameterBag/EnvPlaceholderParameterBagTest.php | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Tests/Compiler/DecoratorServicePassTest.php b/Tests/Compiler/DecoratorServicePassTest.php index 48ed32df6..9858a10e7 100644 --- a/Tests/Compiler/DecoratorServicePassTest.php +++ b/Tests/Compiler/DecoratorServicePassTest.php @@ -197,7 +197,7 @@ public function testProcessMovesTagsFromDecoratedDefinitionToDecoratingDefinitio $this->process($container); - $this->assertEmpty($container->getDefinition('baz.inner')->getTags()); + $this->assertSame([], $container->getDefinition('baz.inner')->getTags()); $this->assertEquals(['bar' => ['attr' => 'baz'], 'foobar' => ['attr' => 'bar'], 'container.decorator' => [['id' => 'foo', 'inner' => 'baz.inner']]], $container->getDefinition('baz')->getTags()); } @@ -220,7 +220,7 @@ public function testProcessMovesTagsFromDecoratedDefinitionToDecoratingDefinitio $this->process($container); - $this->assertEmpty($container->getDefinition('deco1')->getTags()); + $this->assertSame([], $container->getDefinition('deco1')->getTags()); $this->assertEquals(['bar' => ['attr' => 'baz'], 'container.decorator' => [['id' => 'foo', 'inner' => 'deco1.inner']]], $container->getDefinition('deco2')->getTags()); } diff --git a/Tests/Compiler/PassConfigTest.php b/Tests/Compiler/PassConfigTest.php index 8001c5401..66718b7bb 100644 --- a/Tests/Compiler/PassConfigTest.php +++ b/Tests/Compiler/PassConfigTest.php @@ -45,10 +45,10 @@ public function testPassOrderingWithoutPasses() $config->setOptimizationPasses([]); $config->setRemovingPasses([]); - $this->assertEmpty($config->getBeforeOptimizationPasses()); - $this->assertEmpty($config->getAfterRemovingPasses()); - $this->assertEmpty($config->getBeforeRemovingPasses()); - $this->assertEmpty($config->getOptimizationPasses()); - $this->assertEmpty($config->getRemovingPasses()); + $this->assertSame([], $config->getBeforeOptimizationPasses()); + $this->assertSame([], $config->getAfterRemovingPasses()); + $this->assertSame([], $config->getBeforeRemovingPasses()); + $this->assertSame([], $config->getOptimizationPasses()); + $this->assertSame([], $config->getRemovingPasses()); } } diff --git a/Tests/Compiler/ResolveInstanceofConditionalsPassTest.php b/Tests/Compiler/ResolveInstanceofConditionalsPassTest.php index 78fc261ee..76143fc9b 100644 --- a/Tests/Compiler/ResolveInstanceofConditionalsPassTest.php +++ b/Tests/Compiler/ResolveInstanceofConditionalsPassTest.php @@ -39,7 +39,7 @@ public function testProcess() $parent = '.instanceof.'.parent::class.'.0.foo'; $def = $container->getDefinition('foo'); - $this->assertEmpty($def->getInstanceofConditionals()); + $this->assertSame([], $def->getInstanceofConditionals()); $this->assertInstanceOf(ChildDefinition::class, $def); $this->assertTrue($def->isAutowired()); $this->assertSame($parent, $def->getParent()); @@ -266,10 +266,10 @@ public function testMergeReset() $abstract = $container->getDefinition('.abstract.instanceof.bar'); - $this->assertEmpty($abstract->getArguments()); - $this->assertEmpty($abstract->getMethodCalls()); + $this->assertSame([], $abstract->getArguments()); + $this->assertSame([], $abstract->getMethodCalls()); $this->assertNull($abstract->getDecoratedService()); - $this->assertEmpty($abstract->getTags()); + $this->assertSame([], $abstract->getTags()); $this->assertTrue($abstract->isAbstract()); } diff --git a/Tests/ContainerBuilderTest.php b/Tests/ContainerBuilderTest.php index 882fffd19..44837e36f 100644 --- a/Tests/ContainerBuilderTest.php +++ b/Tests/ContainerBuilderTest.php @@ -1150,7 +1150,7 @@ public function testAddObjectResource() $container->setResourceTracking(false); $container->addObjectResource(new \BarClass()); - $this->assertEmpty($container->getResources(), 'No resources get registered without resource tracking'); + $this->assertSame([], $container->getResources(), 'No resources get registered without resource tracking'); $container->setResourceTracking(true); $container->addObjectResource(new \BarClass()); @@ -1173,7 +1173,7 @@ public function testGetReflectionClass() $container->setResourceTracking(false); $r1 = $container->getReflectionClass('BarClass'); - $this->assertEmpty($container->getResources(), 'No resources get registered without resource tracking'); + $this->assertSame([], $container->getResources(), 'No resources get registered without resource tracking'); $container->setResourceTracking(true); $r2 = $container->getReflectionClass('BarClass'); @@ -1213,7 +1213,7 @@ public function testCompilesClassDefinitionsOfLazyServices() { $container = new ContainerBuilder(); - $this->assertEmpty($container->getResources(), 'No resources get registered without resource tracking'); + $this->assertSame([], $container->getResources(), 'No resources get registered without resource tracking'); $container->register('foo', 'BarClass')->setPublic(true); $container->getDefinition('foo')->setLazy(true); @@ -1372,7 +1372,7 @@ public function testExtensionConfig() $container = new ContainerBuilder(); $configs = $container->getExtensionConfig('foo'); - $this->assertEmpty($configs); + $this->assertSame([], $configs); $first = ['foo' => 'bar']; $container->prependExtensionConfig('foo', $first); diff --git a/Tests/Dumper/PhpDumperTest.php b/Tests/Dumper/PhpDumperTest.php index 95abb7087..fbdc28977 100644 --- a/Tests/Dumper/PhpDumperTest.php +++ b/Tests/Dumper/PhpDumperTest.php @@ -986,7 +986,7 @@ public function testLazyArgumentProvideGenerator() } } - $this->assertEmpty(iterator_to_array($lazyContext->lazyEmptyValues)); + $this->assertSame([], iterator_to_array($lazyContext->lazyEmptyValues)); } public function testNormalizedId() diff --git a/Tests/Loader/YamlFileLoaderTest.php b/Tests/Loader/YamlFileLoaderTest.php index 97866064f..54900e4c3 100644 --- a/Tests/Loader/YamlFileLoaderTest.php +++ b/Tests/Loader/YamlFileLoaderTest.php @@ -850,7 +850,7 @@ public function testAnonymousServicesInInstanceof() $anonymous = $container->getDefinition((string) $args['foo']); $this->assertEquals('Anonymous', $anonymous->getClass()); $this->assertFalse($anonymous->isPublic()); - $this->assertEmpty($anonymous->getInstanceofConditionals()); + $this->assertSame([], $anonymous->getInstanceofConditionals()); $this->assertFalse($container->has('Bar')); } diff --git a/Tests/ParameterBag/EnvPlaceholderParameterBagTest.php b/Tests/ParameterBag/EnvPlaceholderParameterBagTest.php index ee0af9ff3..b6779b450 100644 --- a/Tests/ParameterBag/EnvPlaceholderParameterBagTest.php +++ b/Tests/ParameterBag/EnvPlaceholderParameterBagTest.php @@ -66,7 +66,7 @@ public function testMergeWhereFirstBagIsEmptyWillWork() // initialize placeholder only in second bag $secondBag->get($parameter); - $this->assertEmpty($firstBag->getEnvPlaceholders()); + $this->assertSame([], $firstBag->getEnvPlaceholders()); $firstBag->mergeEnvPlaceholders($secondBag); $mergedPlaceholders = $firstBag->getEnvPlaceholders(); From 5817058d0c99150b5b8ca88200882b1d612f0e53 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 5 Mar 2025 09:16:56 +0100 Subject: [PATCH 11/24] [DependencyInjection] Leverage native lazy objects for lazy services --- CHANGELOG.md | 1 + .../Instantiator/LazyServiceInstantiator.php | 13 +- LazyProxy/PhpDumper/LazyServiceDumper.php | 61 +++++- Tests/ContainerBuilderTest.php | 6 +- Tests/Dumper/PhpDumperTest.php | 70 +++++-- .../Fixtures/includes/autowiring_classes.php | 1 + Tests/Fixtures/includes/foo_lazy.php | 1 + .../Fixtures/php/lazy_autowire_attribute.php | 14 +- .../php/legacy_lazy_autowire_attribute.php | 99 +++++++++ ...egacy_services9_lazy_inlined_factories.txt | 196 ++++++++++++++++++ .../php/legacy_services_dedup_lazy.php | 126 +++++++++++ .../php/legacy_services_non_shared_lazy.php | 76 +++++++ ...gacy_services_non_shared_lazy_as_files.txt | 173 ++++++++++++++++ .../legacy_services_non_shared_lazy_ghost.php | 88 ++++++++ ...legacy_services_non_shared_lazy_public.php | 81 ++++++++ .../php/legacy_services_wither_lazy.php | 86 ++++++++ ...legacy_services_wither_lazy_non_shared.php | 88 ++++++++ .../php/services9_lazy_inlined_factories.txt | 20 +- Tests/Fixtures/php/services_dedup_lazy.php | 32 +-- .../php/services_non_shared_lazy_as_files.txt | 23 +- .../php/services_non_shared_lazy_ghost.php | 14 +- .../php/services_non_shared_lazy_public.php | 14 +- Tests/Fixtures/php/services_wither_lazy.php | 16 +- .../php/services_wither_lazy_non_shared.php | 16 +- .../PhpDumper/LazyServiceDumperTest.php | 2 +- 25 files changed, 1154 insertions(+), 163 deletions(-) create mode 100644 Tests/Fixtures/php/legacy_lazy_autowire_attribute.php create mode 100644 Tests/Fixtures/php/legacy_services9_lazy_inlined_factories.txt create mode 100644 Tests/Fixtures/php/legacy_services_dedup_lazy.php create mode 100644 Tests/Fixtures/php/legacy_services_non_shared_lazy.php create mode 100644 Tests/Fixtures/php/legacy_services_non_shared_lazy_as_files.txt create mode 100644 Tests/Fixtures/php/legacy_services_non_shared_lazy_ghost.php create mode 100644 Tests/Fixtures/php/legacy_services_non_shared_lazy_public.php create mode 100644 Tests/Fixtures/php/legacy_services_wither_lazy.php create mode 100644 Tests/Fixtures/php/legacy_services_wither_lazy_non_shared.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0646d04bc..0551fadb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Don't skip classes with private constructor when autodiscovering * Add `Definition::addExcludeTag()` and `ContainerBuilder::findExcludedServiceIds()` for auto-configuration of classes excluded from the service container + * Leverage native lazy objects when possible for lazy services 7.2 --- diff --git a/LazyProxy/Instantiator/LazyServiceInstantiator.php b/LazyProxy/Instantiator/LazyServiceInstantiator.php index f5e7dead5..107482562 100644 --- a/LazyProxy/Instantiator/LazyServiceInstantiator.php +++ b/LazyProxy/Instantiator/LazyServiceInstantiator.php @@ -29,10 +29,19 @@ public function instantiateProxy(ContainerInterface $container, Definition $defi throw new InvalidArgumentException(\sprintf('Cannot instantiate lazy proxy for service "%s".', $id)); } - if (!class_exists($proxyClass = $dumper->getProxyClass($definition, $asGhostObject), false)) { + if (\PHP_VERSION_ID >= 80400 && $asGhostObject) { + return (new \ReflectionClass($definition->getClass()))->newLazyGhost(static function ($ghost) use ($realInstantiator) { $realInstantiator($ghost); }); + } + + $class = null; + if (!class_exists($proxyClass = $dumper->getProxyClass($definition, $asGhostObject, $class), false)) { eval($dumper->getProxyCode($definition, $id)); } - return $asGhostObject ? $proxyClass::createLazyGhost($realInstantiator) : $proxyClass::createLazyProxy($realInstantiator); + if ($definition->getClass() === $proxyClass) { + return $class->newLazyProxy($realInstantiator); + } + + return \PHP_VERSION_ID < 80400 && $asGhostObject ? $proxyClass::createLazyGhost($realInstantiator) : $proxyClass::createLazyProxy($realInstantiator); } } diff --git a/LazyProxy/PhpDumper/LazyServiceDumper.php b/LazyProxy/PhpDumper/LazyServiceDumper.php index b335fa378..60c6a4d60 100644 --- a/LazyProxy/PhpDumper/LazyServiceDumper.php +++ b/LazyProxy/PhpDumper/LazyServiceDumper.php @@ -56,9 +56,21 @@ public function isProxyCandidate(Definition $definition, ?bool &$asGhostObject = } } + if (\PHP_VERSION_ID < 80400) { + try { + $asGhostObject = (bool) ProxyHelper::generateLazyGhost(new \ReflectionClass($class)); + } catch (LogicException) { + } + + return true; + } + try { - $asGhostObject = (bool) ProxyHelper::generateLazyGhost(new \ReflectionClass($class)); - } catch (LogicException) { + $asGhostObject = (bool) (new \ReflectionClass($class))->newLazyGhost(static fn () => null); + } catch (\Error $e) { + if (__FILE__ !== $e->getFile()) { + throw $e; + } } return true; @@ -76,6 +88,16 @@ public function getProxyFactoryCode(Definition $definition, string $id, string $ $proxyClass = $this->getProxyClass($definition, $asGhostObject); if (!$asGhostObject) { + if ($definition->getClass() === $proxyClass) { + return <<newLazyProxy(static fn () => $factoryCode); + } + + + EOF; + } + return <<createProxy('$proxyClass', static fn () => \\$proxyClass::createLazyProxy(static fn () => $factoryCode)); @@ -85,11 +107,23 @@ public function getProxyFactoryCode(Definition $definition, string $id, string $ EOF; } - $factoryCode = \sprintf('static fn ($proxy) => %s', $factoryCode); + if (\PHP_VERSION_ID < 80400) { + $factoryCode = \sprintf('static fn ($proxy) => %s', $factoryCode); + + return <<createProxy('$proxyClass', static fn () => \\$proxyClass::createLazyGhost($factoryCode)); + } + + + EOF; + } + + $factoryCode = \sprintf('static function ($proxy) use ($container) { %s; }', $factoryCode); return <<createProxy('$proxyClass', static fn () => \\$proxyClass::createLazyGhost($factoryCode)); + $instantiation new \ReflectionClass('$proxyClass')->newLazyGhost($factoryCode); } @@ -104,12 +138,21 @@ public function getProxyCode(Definition $definition, ?string $id = null): string $proxyClass = $this->getProxyClass($definition, $asGhostObject, $class); if ($asGhostObject) { + if (\PHP_VERSION_ID >= 80400) { + return ''; + } + try { return ($class?->isReadOnly() ? 'readonly ' : '').'class '.$proxyClass.ProxyHelper::generateLazyGhost($class); } catch (LogicException $e) { throw new InvalidArgumentException(\sprintf('Cannot generate lazy ghost for service "%s".', $id ?? $definition->getClass()), 0, $e); } } + + if ($definition->getClass() === $proxyClass) { + return ''; + } + $interfaces = []; if ($definition->hasTag('proxy')) { @@ -144,6 +187,16 @@ public function getProxyClass(Definition $definition, bool $asGhostObject, ?\Ref $class = 'object' !== $definition->getClass() ? $definition->getClass() : 'stdClass'; $class = new \ReflectionClass($class); + if (\PHP_VERSION_ID >= 80400) { + if ($asGhostObject) { + return $class->name; + } + + if (!$definition->hasTag('proxy') && !$class->isInterface()) { + return $class->name; + } + } + return preg_replace('/^.*\\\\/', '', $definition->getClass()) .($asGhostObject ? 'Ghost' : 'Proxy') .ucfirst(substr(hash('xxh128', $this->salt.'+'.$class->name.'+'.serialize($definition->getTag('proxy'))), -7)); diff --git a/Tests/ContainerBuilderTest.php b/Tests/ContainerBuilderTest.php index 44837e36f..304cd3e4d 100644 --- a/Tests/ContainerBuilderTest.php +++ b/Tests/ContainerBuilderTest.php @@ -1919,8 +1919,12 @@ public function testLazyWither() $container->compile(); $wither = $container->get('wither'); + if (\PHP_VERSION_ID >= 80400) { + $this->assertTrue((new \ReflectionClass($wither))->isUninitializedLazyObject($wither)); + } else { + $this->assertTrue($wither->resetLazyObject()); + } $this->assertInstanceOf(Foo::class, $wither->foo); - $this->assertTrue($wither->resetLazyObject()); $this->assertInstanceOf(Wither::class, $wither->withFoo1($wither->foo)); } diff --git a/Tests/Dumper/PhpDumperTest.php b/Tests/Dumper/PhpDumperTest.php index fbdc28977..e4b5456dc 100644 --- a/Tests/Dumper/PhpDumperTest.php +++ b/Tests/Dumper/PhpDumperTest.php @@ -340,7 +340,7 @@ public function testDumpAsFilesWithLazyFactoriesInlined() if ('\\' === \DIRECTORY_SEPARATOR) { $dump = str_replace("'.\\DIRECTORY_SEPARATOR.'", '/', $dump); } - $this->assertStringMatchesFormatFile(self::$fixturesPath.'/php/services9_lazy_inlined_factories.txt', $dump); + $this->assertStringMatchesFormatFile(self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'services9_lazy_inlined_factories.txt', $dump); } public function testServicesWithAnonymousFactories() @@ -794,7 +794,7 @@ public function testNonSharedLazy() 'inline_class_loader' => false, ]); $this->assertStringEqualsFile( - self::$fixturesPath.'/php/services_non_shared_lazy_public.php', + self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'services_non_shared_lazy_public.php', '\\' === \DIRECTORY_SEPARATOR ? str_replace("'.\\DIRECTORY_SEPARATOR.'", '/', $dump) : $dump ); eval('?>'.$dump); @@ -802,10 +802,18 @@ public function testNonSharedLazy() $container = new \Symfony_DI_PhpDumper_Service_Non_Shared_Lazy(); $foo1 = $container->get('foo'); - $this->assertTrue($foo1->resetLazyObject()); + if (\PHP_VERSION_ID >= 80400) { + $this->assertTrue((new \ReflectionClass($foo1))->isUninitializedLazyObject($foo1)); + } else { + $this->assertTrue($foo1->resetLazyObject()); + } $foo2 = $container->get('foo'); - $this->assertTrue($foo2->resetLazyObject()); + if (\PHP_VERSION_ID >= 80400) { + $this->assertTrue((new \ReflectionClass($foo2))->isUninitializedLazyObject($foo2)); + } else { + $this->assertTrue($foo2->resetLazyObject()); + } $this->assertNotSame($foo1, $foo2); } @@ -832,7 +840,7 @@ public function testNonSharedLazyAsFiles() $stringDump = print_r($dumps, true); $this->assertStringMatchesFormatFile( - self::$fixturesPath.'/php/services_non_shared_lazy_as_files.txt', + self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'services_non_shared_lazy_as_files.txt', '\\' === \DIRECTORY_SEPARATOR ? str_replace("'.\\DIRECTORY_SEPARATOR.'", '/', $stringDump) : $stringDump ); @@ -844,10 +852,18 @@ public function testNonSharedLazyAsFiles() $container = eval('?>'.$lastDump); $foo1 = $container->get('non_shared_foo'); - $this->assertTrue($foo1->resetLazyObject()); + if (\PHP_VERSION_ID >= 80400) { + $this->assertTrue((new \ReflectionClass($foo1))->isUninitializedLazyObject($foo1)); + } else { + $this->assertTrue($foo1->resetLazyObject()); + } $foo2 = $container->get('non_shared_foo'); - $this->assertTrue($foo2->resetLazyObject()); + if (\PHP_VERSION_ID >= 80400) { + $this->assertTrue((new \ReflectionClass($foo2))->isUninitializedLazyObject($foo2)); + } else { + $this->assertTrue($foo2->resetLazyObject()); + } $this->assertNotSame($foo1, $foo2); } @@ -869,7 +885,7 @@ public function testNonSharedLazyDefinitionReferences(bool $asGhostObject) $dumper->setProxyDumper(new \DummyProxyDumper()); } - $this->assertStringEqualsFile(self::$fixturesPath.'/php/services_non_shared_lazy'.($asGhostObject ? '_ghost' : '').'.php', $dumper->dump()); + $this->assertStringEqualsFile(self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'services_non_shared_lazy'.($asGhostObject ? '_ghost' : '').'.php', $dumper->dump()); } public function testNonSharedDuplicates() @@ -942,7 +958,7 @@ public function testDedupLazyProxy() $dumper = new PhpDumper($container); - $this->assertStringEqualsFile(self::$fixturesPath.'/php/services_dedup_lazy.php', $dumper->dump()); + $this->assertStringEqualsFile(self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'services_dedup_lazy.php', $dumper->dump()); } public function testLazyArgumentProvideGenerator() @@ -1607,14 +1623,18 @@ public function testLazyWither() $container->compile(); $dumper = new PhpDumper($container); $dump = $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Service_Wither_Lazy']); - $this->assertStringEqualsFile(self::$fixturesPath.'/php/services_wither_lazy.php', $dump); + $this->assertStringEqualsFile(self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'services_wither_lazy.php', $dump); eval('?>'.$dump); $container = new \Symfony_DI_PhpDumper_Service_Wither_Lazy(); $wither = $container->get('wither'); + if (\PHP_VERSION_ID >= 80400) { + $this->assertTrue((new \ReflectionClass($wither))->isUninitializedLazyObject($wither)); + } else { + $this->assertTrue($wither->resetLazyObject()); + } $this->assertInstanceOf(Foo::class, $wither->foo); - $this->assertTrue($wither->resetLazyObject()); } public function testLazyWitherNonShared() @@ -1632,18 +1652,26 @@ public function testLazyWitherNonShared() $container->compile(); $dumper = new PhpDumper($container); $dump = $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Service_Wither_Lazy_Non_Shared']); - $this->assertStringEqualsFile(self::$fixturesPath.'/php/services_wither_lazy_non_shared.php', $dump); + $this->assertStringEqualsFile(self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'services_wither_lazy_non_shared.php', $dump); eval('?>'.$dump); $container = new \Symfony_DI_PhpDumper_Service_Wither_Lazy_Non_Shared(); $wither1 = $container->get('wither'); + if (\PHP_VERSION_ID >= 80400) { + $this->assertTrue((new \ReflectionClass($wither1))->isUninitializedLazyObject($wither1)); + } else { + $this->assertTrue($wither1->resetLazyObject()); + } $this->assertInstanceOf(Foo::class, $wither1->foo); - $this->assertTrue($wither1->resetLazyObject()); $wither2 = $container->get('wither'); + if (\PHP_VERSION_ID >= 80400) { + $this->assertTrue((new \ReflectionClass($wither2))->isUninitializedLazyObject($wither2)); + } else { + $this->assertTrue($wither2->resetLazyObject()); + } $this->assertInstanceOf(Foo::class, $wither2->foo); - $this->assertTrue($wither2->resetLazyObject()); $this->assertNotSame($wither1, $wither2); } @@ -1971,15 +1999,21 @@ public function testLazyAutowireAttribute() $container->compile(); $dumper = new PhpDumper($container); - $this->assertStringEqualsFile(self::$fixturesPath.'/php/lazy_autowire_attribute.php', $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_Lazy_Autowire_Attribute'])); + $this->assertStringEqualsFile(self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'lazy_autowire_attribute.php', $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_Lazy_Autowire_Attribute'])); - require self::$fixturesPath.'/php/lazy_autowire_attribute.php'; + require self::$fixturesPath.'/php/'.(\PHP_VERSION_ID < 80400 ? 'legacy_' : '').'lazy_autowire_attribute.php'; $container = new \Symfony_DI_PhpDumper_Test_Lazy_Autowire_Attribute(); $this->assertInstanceOf(Foo::class, $container->get('bar')->foo); - $this->assertInstanceOf(LazyObjectInterface::class, $container->get('bar')->foo); - $this->assertSame($container->get('foo'), $container->get('bar')->foo->initializeLazyObject()); + if (\PHP_VERSION_ID >= 80400) { + $r = new \ReflectionClass(Foo::class); + $this->assertTrue($r->isUninitializedLazyObject($container->get('bar')->foo)); + $this->assertSame($container->get('foo'), $r->initializeLazyObject($container->get('bar')->foo)); + } else { + $this->assertInstanceOf(LazyObjectInterface::class, $container->get('bar')->foo); + $this->assertSame($container->get('foo'), $container->get('bar')->foo->initializeLazyObject()); + } } public function testLazyAutowireAttributeWithIntersection() diff --git a/Tests/Fixtures/includes/autowiring_classes.php b/Tests/Fixtures/includes/autowiring_classes.php index 7349cb1a0..d72d7b3ae 100644 --- a/Tests/Fixtures/includes/autowiring_classes.php +++ b/Tests/Fixtures/includes/autowiring_classes.php @@ -14,6 +14,7 @@ class Foo { public static int $counter = 0; + public int $foo = 0; #[Required] public function cloneFoo(?\stdClass $bar = null): static diff --git a/Tests/Fixtures/includes/foo_lazy.php b/Tests/Fixtures/includes/foo_lazy.php index 1caad4b20..e150e09e4 100644 --- a/Tests/Fixtures/includes/foo_lazy.php +++ b/Tests/Fixtures/includes/foo_lazy.php @@ -4,4 +4,5 @@ class FooLazyClass { + public int $foo = 0; } diff --git a/Tests/Fixtures/php/lazy_autowire_attribute.php b/Tests/Fixtures/php/lazy_autowire_attribute.php index 4f596a2b9..97388c0ef 100644 --- a/Tests/Fixtures/php/lazy_autowire_attribute.php +++ b/Tests/Fixtures/php/lazy_autowire_attribute.php @@ -77,21 +77,9 @@ protected static function getFooService($container) protected static function getFoo2Service($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->privates['.lazy.Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] = $container->createProxy('FooProxyCd8d23a', static fn () => \FooProxyCd8d23a::createLazyProxy(static fn () => self::getFoo2Service($container, false))); + return $container->privates['.lazy.Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] = new \ReflectionClass('Symfony\Component\DependencyInjection\Tests\Compiler\Foo')->newLazyProxy(static fn () => self::getFoo2Service($container, false)); } return ($container->services['foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()); } } - -class FooProxyCd8d23a extends \Symfony\Component\DependencyInjection\Tests\Compiler\Foo implements \Symfony\Component\VarExporter\LazyObjectInterface -{ - use \Symfony\Component\VarExporter\LazyProxyTrait; - - private const LAZY_OBJECT_PROPERTY_SCOPES = []; -} - -// Help opcache.preload discover always-needed symbols -class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); -class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); -class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/Tests/Fixtures/php/legacy_lazy_autowire_attribute.php b/Tests/Fixtures/php/legacy_lazy_autowire_attribute.php new file mode 100644 index 000000000..6cf1c86a5 --- /dev/null +++ b/Tests/Fixtures/php/legacy_lazy_autowire_attribute.php @@ -0,0 +1,99 @@ +services = $this->privates = []; + $this->methodMap = [ + 'bar' => 'getBarService', + 'foo' => 'getFooService', + ]; + + $this->aliases = []; + } + + public function compile(): void + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + public function isCompiled(): bool + { + return true; + } + + public function getRemovedIds(): array + { + return [ + 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo' => true, + ]; + } + + protected function createProxy($class, \Closure $factory) + { + return $factory(); + } + + /** + * Gets the public 'bar' shared autowired service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Dumper\LazyServiceConsumer + */ + protected static function getBarService($container) + { + return $container->services['bar'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\LazyServiceConsumer(($container->privates['.lazy.Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] ?? self::getFoo2Service($container))); + } + + /** + * Gets the public 'foo' shared service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Compiler\Foo + */ + protected static function getFooService($container) + { + return $container->services['foo'] = new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo(); + } + + /** + * Gets the private '.lazy.Symfony\Component\DependencyInjection\Tests\Compiler\Foo' shared service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Compiler\Foo + */ + protected static function getFoo2Service($container, $lazyLoad = true) + { + if (true === $lazyLoad) { + return $container->privates['.lazy.Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] = $container->createProxy('FooProxyCd8d23a', static fn () => \FooProxyCd8d23a::createLazyProxy(static fn () => self::getFoo2Service($container, false))); + } + + return ($container->services['foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()); + } +} + +class FooProxyCd8d23a extends \Symfony\Component\DependencyInjection\Tests\Compiler\Foo implements \Symfony\Component\VarExporter\LazyObjectInterface +{ + use \Symfony\Component\VarExporter\LazyProxyTrait; + + private const LAZY_OBJECT_PROPERTY_SCOPES = [ + 'foo' => [parent::class, 'foo', null, 4], + ]; +} + +// Help opcache.preload discover always-needed symbols +class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/Tests/Fixtures/php/legacy_services9_lazy_inlined_factories.txt b/Tests/Fixtures/php/legacy_services9_lazy_inlined_factories.txt new file mode 100644 index 000000000..f945fdd50 --- /dev/null +++ b/Tests/Fixtures/php/legacy_services9_lazy_inlined_factories.txt @@ -0,0 +1,196 @@ +Array +( + [Container%s/proxy-classes.php] => targetDir.''.'/Fixtures/includes/foo.php'; + +class FooClassGhost1728205 extends \Bar\FooClass implements \Symfony\Component\VarExporter\LazyObjectInterface +%A + +if (!\class_exists('FooClassGhost1728205', false)) { + \class_alias(__NAMESPACE__.'\\FooClassGhost1728205', 'FooClassGhost1728205', false); +} + + [Container%s/ProjectServiceContainer.php] => targetDir = \dirname($containerDir); + $this->parameters = $this->getDefaultParameters(); + + $this->services = $this->privates = []; + $this->methodMap = [ + 'lazy_foo' => 'getLazyFooService', + ]; + + $this->aliases = []; + + $this->privates['service_container'] = static function ($container) { + include_once __DIR__.'/proxy-classes.php'; + }; + } + + public function compile(): void + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + public function isCompiled(): bool + { + return true; + } + + protected function createProxy($class, \Closure $factory) + { + return $factory(); + } + + /** + * Gets the public 'lazy_foo' shared service. + * + * @return \Bar\FooClass + */ + protected static function getLazyFooService($container, $lazyLoad = true) + { + if (true === $lazyLoad) { + return $container->services['lazy_foo'] = $container->createProxy('FooClassGhost1728205', static fn () => \FooClassGhost1728205::createLazyGhost(static fn ($proxy) => self::getLazyFooService($container, $proxy))); + } + + include_once $container->targetDir.''.'/Fixtures/includes/foo_lazy.php'; + + return ($lazyLoad->__construct(new \Bar\FooLazyClass()) && false ?: $lazyLoad); + } + + public function getParameter(string $name): array|bool|string|int|float|\UnitEnum|null + { + if (isset($this->buildParameters[$name])) { + return $this->buildParameters[$name]; + } + + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || \array_key_exists($name, $this->parameters))) { + throw new ParameterNotFoundException($name); + } + + if (isset($this->loadedDynamicParameters[$name])) { + $value = $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } else { + $value = $this->parameters[$name]; + } + + return $value; + } + + public function hasParameter(string $name): bool + { + if (isset($this->buildParameters[$name])) { + return true; + } + + return isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || \array_key_exists($name, $this->parameters); + } + + public function setParameter(string $name, $value): void + { + throw new LogicException('Impossible to call set() on a frozen ParameterBag.'); + } + + public function getParameterBag(): ParameterBagInterface + { + if (!isset($this->parameterBag)) { + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + foreach ($this->buildParameters as $name => $value) { + $parameters[$name] = $value; + } + $this->parameterBag = new FrozenParameterBag($parameters, []); + } + + return $this->parameterBag; + } + + private $loadedDynamicParameters = []; + private $dynamicParameters = []; + + private function getDynamicParameter(string $name) + { + throw new ParameterNotFoundException($name); + } + + protected function getDefaultParameters(): array + { + return [ + 'lazy_foo_class' => 'Bar\\FooClass', + 'container.dumper.inline_factories' => true, + 'container.dumper.inline_class_loader' => true, + ]; + } +} + + [ProjectServiceContainer.preload.php] => = 7.4 when preloading is desired + +use Symfony\Component\DependencyInjection\Dumper\Preloader; + +if (in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { + return; +} + +require dirname(__DIR__, %d).'%svendor/autoload.php'; +(require __DIR__.'/ProjectServiceContainer.php')->set(\Container%s\ProjectServiceContainer::class, null); + +$classes = []; +$classes[] = 'Bar\FooClass'; +$classes[] = 'Bar\FooLazyClass'; +$classes[] = 'Symfony\Component\DependencyInjection\ContainerInterface'; + +$preloaded = Preloader::preload($classes); + + [ProjectServiceContainer.php] => '%s', + 'container.build_id' => '%s', + 'container.build_time' => 1563381341, + 'container.runtime_mode' => \in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) ? 'web=0' : 'web=1', +], __DIR__.\DIRECTORY_SEPARATOR.'Container%s'); + +) diff --git a/Tests/Fixtures/php/legacy_services_dedup_lazy.php b/Tests/Fixtures/php/legacy_services_dedup_lazy.php new file mode 100644 index 000000000..60add492b --- /dev/null +++ b/Tests/Fixtures/php/legacy_services_dedup_lazy.php @@ -0,0 +1,126 @@ +services = $this->privates = []; + $this->methodMap = [ + 'bar' => 'getBarService', + 'baz' => 'getBazService', + 'buz' => 'getBuzService', + 'foo' => 'getFooService', + ]; + + $this->aliases = []; + } + + public function compile(): void + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + public function isCompiled(): bool + { + return true; + } + + protected function createProxy($class, \Closure $factory) + { + return $factory(); + } + + /** + * Gets the public 'bar' shared service. + * + * @return \stdClass + */ + protected static function getBarService($container, $lazyLoad = true) + { + if (true === $lazyLoad) { + return $container->services['bar'] = $container->createProxy('stdClassGhostAa01f12', static fn () => \stdClassGhostAa01f12::createLazyGhost(static fn ($proxy) => self::getBarService($container, $proxy))); + } + + return $lazyLoad; + } + + /** + * Gets the public 'baz' shared service. + * + * @return \stdClass + */ + protected static function getBazService($container, $lazyLoad = true) + { + if (true === $lazyLoad) { + return $container->services['baz'] = $container->createProxy('stdClassProxyAa01f12', static fn () => \stdClassProxyAa01f12::createLazyProxy(static fn () => self::getBazService($container, false))); + } + + return \foo_bar(); + } + + /** + * Gets the public 'buz' shared service. + * + * @return \stdClass + */ + protected static function getBuzService($container, $lazyLoad = true) + { + if (true === $lazyLoad) { + return $container->services['buz'] = $container->createProxy('stdClassProxyAa01f12', static fn () => \stdClassProxyAa01f12::createLazyProxy(static fn () => self::getBuzService($container, false))); + } + + return \foo_bar(); + } + + /** + * Gets the public 'foo' shared service. + * + * @return \stdClass + */ + protected static function getFooService($container, $lazyLoad = true) + { + if (true === $lazyLoad) { + return $container->services['foo'] = $container->createProxy('stdClassGhostAa01f12', static fn () => \stdClassGhostAa01f12::createLazyGhost(static fn ($proxy) => self::getFooService($container, $proxy))); + } + + return $lazyLoad; + } +} + +class stdClassGhostAa01f12 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface +{ + use \Symfony\Component\VarExporter\LazyGhostTrait; + + private const LAZY_OBJECT_PROPERTY_SCOPES = []; +} + +// Help opcache.preload discover always-needed symbols +class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); + +class stdClassProxyAa01f12 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface +{ + use \Symfony\Component\VarExporter\LazyProxyTrait; + + private const LAZY_OBJECT_PROPERTY_SCOPES = []; +} + +// Help opcache.preload discover always-needed symbols +class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/Tests/Fixtures/php/legacy_services_non_shared_lazy.php b/Tests/Fixtures/php/legacy_services_non_shared_lazy.php new file mode 100644 index 000000000..f584bef6b --- /dev/null +++ b/Tests/Fixtures/php/legacy_services_non_shared_lazy.php @@ -0,0 +1,76 @@ +services = $this->privates = []; + $this->methodMap = [ + 'bar' => 'getBarService', + ]; + + $this->aliases = []; + } + + public function compile(): void + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + public function isCompiled(): bool + { + return true; + } + + public function getRemovedIds(): array + { + return [ + 'foo' => true, + ]; + } + + protected function createProxy($class, \Closure $factory) + { + return $factory(); + } + + /** + * Gets the public 'bar' shared service. + * + * @return \stdClass + */ + protected static function getBarService($container) + { + return $container->services['bar'] = new \stdClass((isset($container->factories['service_container']['foo']) ? $container->factories['service_container']['foo']($container) : self::getFooService($container))); + } + + /** + * Gets the private 'foo' service. + * + * @return \stdClass + */ + protected static function getFooService($container, $lazyLoad = true) + { + $container->factories['service_container']['foo'] ??= self::getFooService(...); + + // lazy factory for stdClass + + return new \stdClass(); + } +} + +// proxy code for stdClass diff --git a/Tests/Fixtures/php/legacy_services_non_shared_lazy_as_files.txt b/Tests/Fixtures/php/legacy_services_non_shared_lazy_as_files.txt new file mode 100644 index 000000000..d52dd5a7b --- /dev/null +++ b/Tests/Fixtures/php/legacy_services_non_shared_lazy_as_files.txt @@ -0,0 +1,173 @@ +Array +( + [Container%s/getNonSharedFooService.php] => factories['non_shared_foo'] ??= fn () => self::do($container); + + if (true === $lazyLoad) { + return $container->createProxy('FooLazyClassGhost%s', static fn () => \FooLazyClassGhost%s::createLazyGhost(static fn ($proxy) => self::do($container, $proxy))); + } + + static $include = true; + + if ($include) { + include_once '%sfoo_lazy.php'; + + $include = false; + } + + return $lazyLoad; + } +} + + [Container%s/FooLazyClassGhost%s.php] => [parent::class, 'foo', null, 4], + ]; +} + +// Help opcache.preload discover always-needed symbols +class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); + +if (!\class_exists('FooLazyClassGhost%s', false)) { + \class_alias(__NAMESPACE__.'\\FooLazyClassGhost%s', 'FooLazyClassGhost%s', false); +} + + [Container%s/Symfony_DI_PhpDumper_Service_Non_Shared_Lazy_As_File.php] => services = $this->privates = []; + $this->fileMap = [ + 'non_shared_foo' => 'getNonSharedFooService', + ]; + + $this->aliases = []; + } + + public function compile(): void + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + public function isCompiled(): bool + { + return true; + } + + protected function load($file, $lazyLoad = true): mixed + { + if (class_exists($class = __NAMESPACE__.'\\'.$file, false)) { + return $class::do($this, $lazyLoad); + } + + if ('.' === $file[-4]) { + $class = substr($class, 0, -4); + } else { + $file .= '.php'; + } + + $service = require $this->containerDir.\DIRECTORY_SEPARATOR.$file; + + return class_exists($class, false) ? $class::do($this, $lazyLoad) : $service; + } + + protected function createProxy($class, \Closure $factory) + { + class_exists($class, false) || require __DIR__.'/'.$class.'.php'; + + return $factory(); + } +} + + [Symfony_DI_PhpDumper_Service_Non_Shared_Lazy_As_File.preload.php] => = 7.4 when preloading is desired + +use Symfony\Component\DependencyInjection\Dumper\Preloader; + +if (in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { + return; +} + +require '%svendor/autoload.php'; +(require __DIR__.'/Symfony_DI_PhpDumper_Service_Non_Shared_Lazy_As_File.php')->set(\Container%s\Symfony_DI_PhpDumper_Service_Non_Shared_Lazy_As_File::class, null); +require __DIR__.'/Container%s/FooLazyClassGhost%s.php'; +require __DIR__.'/Container%s/getNonSharedFooService.php'; + +$classes = []; +$classes[] = 'Bar\FooLazyClass'; +$classes[] = 'Symfony\Component\DependencyInjection\ContainerInterface'; + +$preloaded = Preloader::preload($classes); + + [Symfony_DI_PhpDumper_Service_Non_Shared_Lazy_As_File.php] => '%s', + 'container.build_id' => '%s', + 'container.build_time' => %d, + 'container.runtime_mode' => \in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) ? 'web=0' : 'web=1', +], __DIR__.\DIRECTORY_SEPARATOR.'Container%s'); + +) diff --git a/Tests/Fixtures/php/legacy_services_non_shared_lazy_ghost.php b/Tests/Fixtures/php/legacy_services_non_shared_lazy_ghost.php new file mode 100644 index 000000000..b03463295 --- /dev/null +++ b/Tests/Fixtures/php/legacy_services_non_shared_lazy_ghost.php @@ -0,0 +1,88 @@ +services = $this->privates = []; + $this->methodMap = [ + 'bar' => 'getBarService', + ]; + + $this->aliases = []; + } + + public function compile(): void + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + public function isCompiled(): bool + { + return true; + } + + public function getRemovedIds(): array + { + return [ + 'foo' => true, + ]; + } + + protected function createProxy($class, \Closure $factory) + { + return $factory(); + } + + /** + * Gets the public 'bar' shared service. + * + * @return \stdClass + */ + protected static function getBarService($container) + { + return $container->services['bar'] = new \stdClass((isset($container->factories['service_container']['foo']) ? $container->factories['service_container']['foo']($container) : self::getFooService($container))); + } + + /** + * Gets the private 'foo' service. + * + * @return \stdClass + */ + protected static function getFooService($container, $lazyLoad = true) + { + $container->factories['service_container']['foo'] ??= self::getFooService(...); + + if (true === $lazyLoad) { + return $container->createProxy('stdClassGhostAa01f12', static fn () => \stdClassGhostAa01f12::createLazyGhost(static fn ($proxy) => self::getFooService($container, $proxy))); + } + + return $lazyLoad; + } +} + +class stdClassGhostAa01f12 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface +{ + use \Symfony\Component\VarExporter\LazyGhostTrait; + + private const LAZY_OBJECT_PROPERTY_SCOPES = []; +} + +// Help opcache.preload discover always-needed symbols +class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/Tests/Fixtures/php/legacy_services_non_shared_lazy_public.php b/Tests/Fixtures/php/legacy_services_non_shared_lazy_public.php new file mode 100644 index 000000000..0841cf192 --- /dev/null +++ b/Tests/Fixtures/php/legacy_services_non_shared_lazy_public.php @@ -0,0 +1,81 @@ +services = $this->privates = []; + $this->methodMap = [ + 'foo' => 'getFooService', + ]; + + $this->aliases = []; + } + + public function compile(): void + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + public function isCompiled(): bool + { + return true; + } + + protected function createProxy($class, \Closure $factory) + { + return $factory(); + } + + /** + * Gets the public 'foo' service. + * + * @return \Bar\FooLazyClass + */ + protected static function getFooService($container, $lazyLoad = true) + { + $container->factories['foo'] ??= fn () => self::getFooService($container); + + if (true === $lazyLoad) { + return $container->createProxy('FooLazyClassGhost82ad1a4', static fn () => \FooLazyClassGhost82ad1a4::createLazyGhost(static fn ($proxy) => self::getFooService($container, $proxy))); + } + + static $include = true; + + if ($include) { + include_once __DIR__.'/Fixtures/includes/foo_lazy.php'; + + $include = false; + } + + return $lazyLoad; + } +} + +class FooLazyClassGhost82ad1a4 extends \Bar\FooLazyClass implements \Symfony\Component\VarExporter\LazyObjectInterface +{ + use \Symfony\Component\VarExporter\LazyGhostTrait; + + private const LAZY_OBJECT_PROPERTY_SCOPES = [ + 'foo' => [parent::class, 'foo', null, 4], + ]; +} + +// Help opcache.preload discover always-needed symbols +class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/Tests/Fixtures/php/legacy_services_wither_lazy.php b/Tests/Fixtures/php/legacy_services_wither_lazy.php new file mode 100644 index 000000000..b9e916457 --- /dev/null +++ b/Tests/Fixtures/php/legacy_services_wither_lazy.php @@ -0,0 +1,86 @@ +services = $this->privates = []; + $this->methodMap = [ + 'wither' => 'getWitherService', + ]; + + $this->aliases = []; + } + + public function compile(): void + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + public function isCompiled(): bool + { + return true; + } + + public function getRemovedIds(): array + { + return [ + 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo' => true, + ]; + } + + protected function createProxy($class, \Closure $factory) + { + return $factory(); + } + + /** + * Gets the public 'wither' shared autowired service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Compiler\Wither + */ + protected static function getWitherService($container, $lazyLoad = true) + { + if (true === $lazyLoad) { + return $container->services['wither'] = $container->createProxy('WitherProxy1991f2a', static fn () => \WitherProxy1991f2a::createLazyProxy(static fn () => self::getWitherService($container, false))); + } + + $instance = new \Symfony\Component\DependencyInjection\Tests\Compiler\Wither(); + + $a = ($container->privates['Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()); + + $instance = $instance->withFoo1($a); + $instance = $instance->withFoo2($a); + $instance->setFoo($a); + + return $instance; + } +} + +class WitherProxy1991f2a extends \Symfony\Component\DependencyInjection\Tests\Compiler\Wither implements \Symfony\Component\VarExporter\LazyObjectInterface +{ + use \Symfony\Component\VarExporter\LazyProxyTrait; + + private const LAZY_OBJECT_PROPERTY_SCOPES = [ + 'foo' => [parent::class, 'foo', null, 4], + ]; +} + +// Help opcache.preload discover always-needed symbols +class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/Tests/Fixtures/php/legacy_services_wither_lazy_non_shared.php b/Tests/Fixtures/php/legacy_services_wither_lazy_non_shared.php new file mode 100644 index 000000000..d70588f65 --- /dev/null +++ b/Tests/Fixtures/php/legacy_services_wither_lazy_non_shared.php @@ -0,0 +1,88 @@ +services = $this->privates = []; + $this->methodMap = [ + 'wither' => 'getWitherService', + ]; + + $this->aliases = []; + } + + public function compile(): void + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + public function isCompiled(): bool + { + return true; + } + + public function getRemovedIds(): array + { + return [ + 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo' => true, + ]; + } + + protected function createProxy($class, \Closure $factory) + { + return $factory(); + } + + /** + * Gets the public 'wither' autowired service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Compiler\Wither + */ + protected static function getWitherService($container, $lazyLoad = true) + { + $container->factories['wither'] ??= fn () => self::getWitherService($container); + + if (true === $lazyLoad) { + return $container->createProxy('WitherProxyE94fdba', static fn () => \WitherProxyE94fdba::createLazyProxy(static fn () => self::getWitherService($container, false))); + } + + $instance = new \Symfony\Component\DependencyInjection\Tests\Compiler\Wither(); + + $a = ($container->privates['Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()); + + $instance = $instance->withFoo1($a); + $instance = $instance->withFoo2($a); + $instance->setFoo($a); + + return $instance; + } +} + +class WitherProxyE94fdba extends \Symfony\Component\DependencyInjection\Tests\Compiler\Wither implements \Symfony\Component\VarExporter\LazyObjectInterface +{ + use \Symfony\Component\VarExporter\LazyProxyTrait; + + private const LAZY_OBJECT_PROPERTY_SCOPES = [ + 'foo' => [parent::class, 'foo', null, 4], + ]; +} + +// Help opcache.preload discover always-needed symbols +class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/Tests/Fixtures/php/services9_lazy_inlined_factories.txt b/Tests/Fixtures/php/services9_lazy_inlined_factories.txt index f945fdd50..9e6a3865f 100644 --- a/Tests/Fixtures/php/services9_lazy_inlined_factories.txt +++ b/Tests/Fixtures/php/services9_lazy_inlined_factories.txt @@ -1,18 +1,5 @@ Array ( - [Container%s/proxy-classes.php] => targetDir.''.'/Fixtures/includes/foo.php'; - -class FooClassGhost1728205 extends \Bar\FooClass implements \Symfony\Component\VarExporter\LazyObjectInterface -%A - -if (!\class_exists('FooClassGhost1728205', false)) { - \class_alias(__NAMESPACE__.'\\FooClassGhost1728205', 'FooClassGhost1728205', false); -} - [Container%s/ProjectServiceContainer.php] => aliases = []; - - $this->privates['service_container'] = static function ($container) { - include_once __DIR__.'/proxy-classes.php'; - }; } public function compile(): void @@ -74,9 +57,10 @@ class ProjectServiceContainer extends Container protected static function getLazyFooService($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->services['lazy_foo'] = $container->createProxy('FooClassGhost1728205', static fn () => \FooClassGhost1728205::createLazyGhost(static fn ($proxy) => self::getLazyFooService($container, $proxy))); + return $container->services['lazy_foo'] = new \ReflectionClass('Bar\FooClass')->newLazyGhost(static function ($proxy) use ($container) { self::getLazyFooService($container, $proxy); }); } + include_once $container->targetDir.''.'/Fixtures/includes/foo.php'; include_once $container->targetDir.''.'/Fixtures/includes/foo_lazy.php'; return ($lazyLoad->__construct(new \Bar\FooLazyClass()) && false ?: $lazyLoad); diff --git a/Tests/Fixtures/php/services_dedup_lazy.php b/Tests/Fixtures/php/services_dedup_lazy.php index 60add492b..413a883e5 100644 --- a/Tests/Fixtures/php/services_dedup_lazy.php +++ b/Tests/Fixtures/php/services_dedup_lazy.php @@ -52,7 +52,7 @@ protected function createProxy($class, \Closure $factory) protected static function getBarService($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->services['bar'] = $container->createProxy('stdClassGhostAa01f12', static fn () => \stdClassGhostAa01f12::createLazyGhost(static fn ($proxy) => self::getBarService($container, $proxy))); + return $container->services['bar'] = new \ReflectionClass('stdClass')->newLazyGhost(static function ($proxy) use ($container) { self::getBarService($container, $proxy); }); } return $lazyLoad; @@ -66,7 +66,7 @@ protected static function getBarService($container, $lazyLoad = true) protected static function getBazService($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->services['baz'] = $container->createProxy('stdClassProxyAa01f12', static fn () => \stdClassProxyAa01f12::createLazyProxy(static fn () => self::getBazService($container, false))); + return $container->services['baz'] = new \ReflectionClass('stdClass')->newLazyProxy(static fn () => self::getBazService($container, false)); } return \foo_bar(); @@ -80,7 +80,7 @@ protected static function getBazService($container, $lazyLoad = true) protected static function getBuzService($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->services['buz'] = $container->createProxy('stdClassProxyAa01f12', static fn () => \stdClassProxyAa01f12::createLazyProxy(static fn () => self::getBuzService($container, false))); + return $container->services['buz'] = new \ReflectionClass('stdClass')->newLazyProxy(static fn () => self::getBuzService($container, false)); } return \foo_bar(); @@ -94,33 +94,9 @@ protected static function getBuzService($container, $lazyLoad = true) protected static function getFooService($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->services['foo'] = $container->createProxy('stdClassGhostAa01f12', static fn () => \stdClassGhostAa01f12::createLazyGhost(static fn ($proxy) => self::getFooService($container, $proxy))); + return $container->services['foo'] = new \ReflectionClass('stdClass')->newLazyGhost(static function ($proxy) use ($container) { self::getFooService($container, $proxy); }); } return $lazyLoad; } } - -class stdClassGhostAa01f12 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface -{ - use \Symfony\Component\VarExporter\LazyGhostTrait; - - private const LAZY_OBJECT_PROPERTY_SCOPES = []; -} - -// Help opcache.preload discover always-needed symbols -class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); -class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); -class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); - -class stdClassProxyAa01f12 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface -{ - use \Symfony\Component\VarExporter\LazyProxyTrait; - - private const LAZY_OBJECT_PROPERTY_SCOPES = []; -} - -// Help opcache.preload discover always-needed symbols -class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); -class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); -class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt b/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt index 488895d7c..69a47220c 100644 --- a/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt +++ b/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt @@ -23,7 +23,7 @@ class getNonSharedFooService extends Symfony_DI_PhpDumper_Service_Non_Shared_Laz $container->factories['non_shared_foo'] ??= fn () => self::do($container); if (true === $lazyLoad) { - return $container->createProxy('FooLazyClassGhost%s', static fn () => \FooLazyClassGhost%s::createLazyGhost(static fn ($proxy) => self::do($container, $proxy))); + return new \ReflectionClass('Bar\FooLazyClass')->newLazyGhost(static function ($proxy) use ($container) { self::do($container, $proxy); }); } static $include = true; @@ -38,26 +38,6 @@ class getNonSharedFooService extends Symfony_DI_PhpDumper_Service_Non_Shared_Laz } } - [Container%s/FooLazyClassGhost%s.php] => set(\Container%s\Symfony_DI_PhpDumper_Service_Non_Shared_Lazy_As_File::class, null); -require __DIR__.'/Container%s/FooLazyClassGhost%s.php'; require __DIR__.'/Container%s/getNonSharedFooService.php'; $classes = []; diff --git a/Tests/Fixtures/php/services_non_shared_lazy_ghost.php b/Tests/Fixtures/php/services_non_shared_lazy_ghost.php index b03463295..281baf615 100644 --- a/Tests/Fixtures/php/services_non_shared_lazy_ghost.php +++ b/Tests/Fixtures/php/services_non_shared_lazy_ghost.php @@ -68,21 +68,9 @@ protected static function getFooService($container, $lazyLoad = true) $container->factories['service_container']['foo'] ??= self::getFooService(...); if (true === $lazyLoad) { - return $container->createProxy('stdClassGhostAa01f12', static fn () => \stdClassGhostAa01f12::createLazyGhost(static fn ($proxy) => self::getFooService($container, $proxy))); + return new \ReflectionClass('stdClass')->newLazyGhost(static function ($proxy) use ($container) { self::getFooService($container, $proxy); }); } return $lazyLoad; } } - -class stdClassGhostAa01f12 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface -{ - use \Symfony\Component\VarExporter\LazyGhostTrait; - - private const LAZY_OBJECT_PROPERTY_SCOPES = []; -} - -// Help opcache.preload discover always-needed symbols -class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); -class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); -class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/Tests/Fixtures/php/services_non_shared_lazy_public.php b/Tests/Fixtures/php/services_non_shared_lazy_public.php index 7f870f886..93b7ee3ed 100644 --- a/Tests/Fixtures/php/services_non_shared_lazy_public.php +++ b/Tests/Fixtures/php/services_non_shared_lazy_public.php @@ -51,7 +51,7 @@ protected static function getFooService($container, $lazyLoad = true) $container->factories['foo'] ??= fn () => self::getFooService($container); if (true === $lazyLoad) { - return $container->createProxy('FooLazyClassGhost82ad1a4', static fn () => \FooLazyClassGhost82ad1a4::createLazyGhost(static fn ($proxy) => self::getFooService($container, $proxy))); + return new \ReflectionClass('Bar\FooLazyClass')->newLazyGhost(static function ($proxy) use ($container) { self::getFooService($container, $proxy); }); } static $include = true; @@ -65,15 +65,3 @@ protected static function getFooService($container, $lazyLoad = true) return $lazyLoad; } } - -class FooLazyClassGhost82ad1a4 extends \Bar\FooLazyClass implements \Symfony\Component\VarExporter\LazyObjectInterface -{ - use \Symfony\Component\VarExporter\LazyGhostTrait; - - private const LAZY_OBJECT_PROPERTY_SCOPES = []; -} - -// Help opcache.preload discover always-needed symbols -class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); -class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); -class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/Tests/Fixtures/php/services_wither_lazy.php b/Tests/Fixtures/php/services_wither_lazy.php index b9e916457..76031f1ca 100644 --- a/Tests/Fixtures/php/services_wither_lazy.php +++ b/Tests/Fixtures/php/services_wither_lazy.php @@ -56,7 +56,7 @@ protected function createProxy($class, \Closure $factory) protected static function getWitherService($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->services['wither'] = $container->createProxy('WitherProxy1991f2a', static fn () => \WitherProxy1991f2a::createLazyProxy(static fn () => self::getWitherService($container, false))); + return $container->services['wither'] = new \ReflectionClass('Symfony\Component\DependencyInjection\Tests\Compiler\Wither')->newLazyProxy(static fn () => self::getWitherService($container, false)); } $instance = new \Symfony\Component\DependencyInjection\Tests\Compiler\Wither(); @@ -70,17 +70,3 @@ protected static function getWitherService($container, $lazyLoad = true) return $instance; } } - -class WitherProxy1991f2a extends \Symfony\Component\DependencyInjection\Tests\Compiler\Wither implements \Symfony\Component\VarExporter\LazyObjectInterface -{ - use \Symfony\Component\VarExporter\LazyProxyTrait; - - private const LAZY_OBJECT_PROPERTY_SCOPES = [ - 'foo' => [parent::class, 'foo', null, 4], - ]; -} - -// Help opcache.preload discover always-needed symbols -class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); -class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); -class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/Tests/Fixtures/php/services_wither_lazy_non_shared.php b/Tests/Fixtures/php/services_wither_lazy_non_shared.php index d70588f65..640955a7e 100644 --- a/Tests/Fixtures/php/services_wither_lazy_non_shared.php +++ b/Tests/Fixtures/php/services_wither_lazy_non_shared.php @@ -58,7 +58,7 @@ protected static function getWitherService($container, $lazyLoad = true) $container->factories['wither'] ??= fn () => self::getWitherService($container); if (true === $lazyLoad) { - return $container->createProxy('WitherProxyE94fdba', static fn () => \WitherProxyE94fdba::createLazyProxy(static fn () => self::getWitherService($container, false))); + return new \ReflectionClass('Symfony\Component\DependencyInjection\Tests\Compiler\Wither')->newLazyProxy(static fn () => self::getWitherService($container, false)); } $instance = new \Symfony\Component\DependencyInjection\Tests\Compiler\Wither(); @@ -72,17 +72,3 @@ protected static function getWitherService($container, $lazyLoad = true) return $instance; } } - -class WitherProxyE94fdba extends \Symfony\Component\DependencyInjection\Tests\Compiler\Wither implements \Symfony\Component\VarExporter\LazyObjectInterface -{ - use \Symfony\Component\VarExporter\LazyProxyTrait; - - private const LAZY_OBJECT_PROPERTY_SCOPES = [ - 'foo' => [parent::class, 'foo', null, 4], - ]; -} - -// Help opcache.preload discover always-needed symbols -class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); -class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); -class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/Tests/LazyProxy/PhpDumper/LazyServiceDumperTest.php b/Tests/LazyProxy/PhpDumper/LazyServiceDumperTest.php index 467972a88..14d2f81b6 100644 --- a/Tests/LazyProxy/PhpDumper/LazyServiceDumperTest.php +++ b/Tests/LazyProxy/PhpDumper/LazyServiceDumperTest.php @@ -63,7 +63,7 @@ public function testReadonlyClass() $definition = (new Definition(ReadOnlyClass::class))->setLazy(true); $this->assertTrue($dumper->isProxyCandidate($definition)); - $this->assertStringContainsString('readonly class ReadOnlyClassGhost', $dumper->getProxyCode($definition)); + $this->assertStringContainsString(\PHP_VERSION_ID >= 80400 ? '' : 'readonly class ReadOnlyClassGhost', $dumper->getProxyCode($definition)); } } From dfb84e016bbb8ea26815e1e2dd84eca372ec81b5 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Thu, 13 Mar 2025 23:58:12 +0100 Subject: [PATCH 12/24] chore: PHP CS Fixer fixes --- Compiler/CheckCircularReferencesPass.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Compiler/CheckCircularReferencesPass.php b/Compiler/CheckCircularReferencesPass.php index 7adb0b4d8..8a7c11383 100644 --- a/Compiler/CheckCircularReferencesPass.php +++ b/Compiler/CheckCircularReferencesPass.php @@ -62,7 +62,7 @@ private function checkOutEdges(array $edges): void continue; } - $isLeaf = !!$node->getValue(); + $isLeaf = (bool) $node->getValue(); $isConcrete = !$edge->isLazy() && !$edge->isWeak(); // Skip already checked lazy services if they are still lazy. Will not gain any new information. From 78fe2635d091518527c3842ee34d4bccc030c642 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Tue, 18 Mar 2025 20:37:55 -0400 Subject: [PATCH 13/24] [DI] Rename "exclude tag" to "resource tag" --- CHANGELOG.md | 2 +- ContainerBuilder.php | 8 ++++---- Definition.php | 6 +++--- Tests/ContainerBuilderTest.php | 14 +++++++------- Tests/DefinitionTest.php | 4 ++-- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0551fadb2..1021c0a25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ CHANGELOG * Make `#[AsTaggedItem]` repeatable * Support `@>` as a shorthand for `!service_closure` in yaml files * Don't skip classes with private constructor when autodiscovering - * Add `Definition::addExcludeTag()` and `ContainerBuilder::findExcludedServiceIds()` + * Add `Definition::addResourceTag()` and `ContainerBuilder::findTaggedResourceIds()` for auto-configuration of classes excluded from the service container * Leverage native lazy objects when possible for lazy services diff --git a/ContainerBuilder.php b/ContainerBuilder.php index f5270e31a..8197b104c 100644 --- a/ContainerBuilder.php +++ b/ContainerBuilder.php @@ -1356,9 +1356,9 @@ public function findTaggedServiceIds(string $name, bool $throwOnAbstract = false * * Example: * - * $container->register('foo')->addExcludeTag('my.tag', ['hello' => 'world']) + * $container->register('foo')->addResourceTag('my.tag', ['hello' => 'world']) * - * $serviceIds = $container->findExcludedServiceIds('my.tag'); + * $serviceIds = $container->findTaggedResourceIds('my.tag'); * foreach ($serviceIds as $serviceId => $tags) { * foreach ($tags as $tag) { * echo $tag['hello']; @@ -1367,14 +1367,14 @@ public function findTaggedServiceIds(string $name, bool $throwOnAbstract = false * * @return array An array of tags with the tagged service as key, holding a list of attribute arrays */ - public function findExcludedServiceIds(string $tagName): array + public function findTaggedResourceIds(string $tagName): array { $this->usedTags[] = $tagName; $tags = []; foreach ($this->getDefinitions() as $id => $definition) { if ($definition->hasTag($tagName)) { if (!$definition->hasTag('container.excluded')) { - throw new InvalidArgumentException(\sprintf('The service "%s" tagged "%s" is missing the "container.excluded" tag.', $id, $tagName)); + throw new InvalidArgumentException(\sprintf('The resource "%s" tagged "%s" is missing the "container.excluded" tag.', $id, $tagName)); } $tags[$id] = $definition->getTag($tagName); } diff --git a/Definition.php b/Definition.php index 682540e91..61cc0b9d6 100644 --- a/Definition.php +++ b/Definition.php @@ -456,13 +456,13 @@ public function addTag(string $name, array $attributes = []): static } /** - * Adds a tag to the definition and marks it as excluded. + * Adds a "resource" tag to the definition and marks it as excluded. * - * These definitions should be processed using {@see ContainerBuilder::findExcludedServiceIds()} + * These definitions should be processed using {@see ContainerBuilder::findTaggedResourceIds()} * * @return $this */ - public function addExcludeTag(string $name, array $attributes = []): static + public function addResourceTag(string $name, array $attributes = []): static { return $this->addTag($name, $attributes) ->addTag('container.excluded', ['source' => \sprintf('by tag "%s"', $name)]) diff --git a/Tests/ContainerBuilderTest.php b/Tests/ContainerBuilderTest.php index 304cd3e4d..ccae8c30e 100644 --- a/Tests/ContainerBuilderTest.php +++ b/Tests/ContainerBuilderTest.php @@ -1095,21 +1095,21 @@ public function testFindTaggedServiceIdsThrowsWhenAbstract() $builder->findTaggedServiceIds('foo', true); } - public function testFindExcludedServiceIds() + public function testFindTaggedResourceIds() { $builder = new ContainerBuilder(); $builder->register('myservice', 'Bar\FooClass') ->addTag('foo', ['foo' => 'foo']) ->addTag('bar', ['bar' => 'bar']) ->addTag('foo', ['foofoo' => 'foofoo']) - ->addExcludeTag('container.excluded'); + ->addResourceTag('container.excluded'); $expected = ['myservice' => [['foo' => 'foo'], ['foofoo' => 'foofoo']]]; - $this->assertSame($expected, $builder->findExcludedServiceIds('foo')); - $this->assertSame([], $builder->findExcludedServiceIds('foofoo')); + $this->assertSame($expected, $builder->findTaggedResourceIds('foo')); + $this->assertSame([], $builder->findTaggedResourceIds('foofoo')); } - public function testFindExcludedServiceIdsThrowsWhenNotExcluded() + public function testFindTaggedResourceIdsThrowsWhenNotExcluded() { $builder = new ContainerBuilder(); $builder->register('myservice', 'Bar\FooClass') @@ -1118,8 +1118,8 @@ public function testFindExcludedServiceIdsThrowsWhenNotExcluded() ->addTag('foo', ['foofoo' => 'foofoo']); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The service "myservice" tagged "foo" is missing the "container.excluded" tag.'); - $builder->findExcludedServiceIds('foo', true); + $this->expectExceptionMessage('The resource "myservice" tagged "foo" is missing the "container.excluded" tag.'); + $builder->findTaggedResourceIds('foo'); } public function testFindUnusedTags() diff --git a/Tests/DefinitionTest.php b/Tests/DefinitionTest.php index 1a51c9af3..459e566d2 100644 --- a/Tests/DefinitionTest.php +++ b/Tests/DefinitionTest.php @@ -258,10 +258,10 @@ public function testTags() ], $def->getTags(), '->getTags() returns all tags'); } - public function testAddExcludeTag() + public function testAddResourceTag() { $def = new Definition('stdClass'); - $def->addExcludeTag('foo', ['bar' => true]); + $def->addResourceTag('foo', ['bar' => true]); $this->assertSame([['bar' => true]], $def->getTag('foo')); $this->assertTrue($def->isAbstract()); From f4533217d13bf6bc9aba5a870e7338e35aaa828f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 18 Mar 2025 17:29:14 +0100 Subject: [PATCH 14/24] [DependencyInjection] Enable multiple attribute autoconfiguration callbacks on the same class --- CHANGELOG.md | 1 + Compiler/AttributeAutoconfigurationPass.php | 130 ++++++++++---------- ContainerBuilder.php | 38 ++++-- Tests/ContainerBuilderTest.php | 32 +++++ 4 files changed, 124 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1021c0a25..07521bc86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Don't skip classes with private constructor when autodiscovering * Add `Definition::addResourceTag()` and `ContainerBuilder::findTaggedResourceIds()` for auto-configuration of classes excluded from the service container + * Accept multiple auto-configuration callbacks for the same attribute class * Leverage native lazy objects when possible for lazy services 7.2 diff --git a/Compiler/AttributeAutoconfigurationPass.php b/Compiler/AttributeAutoconfigurationPass.php index 9c3b98eab..9c0eec543 100644 --- a/Compiler/AttributeAutoconfigurationPass.php +++ b/Compiler/AttributeAutoconfigurationPass.php @@ -31,49 +31,51 @@ final class AttributeAutoconfigurationPass extends AbstractRecursivePass public function process(ContainerBuilder $container): void { - if (!$container->getAutoconfiguredAttributes()) { + if (!$container->getAttributeAutoconfigurators()) { return; } - foreach ($container->getAutoconfiguredAttributes() as $attributeName => $callable) { - $callableReflector = new \ReflectionFunction($callable(...)); - if ($callableReflector->getNumberOfParameters() <= 2) { - $this->classAttributeConfigurators[$attributeName] = $callable; - continue; - } + foreach ($container->getAttributeAutoconfigurators() as $attributeName => $callables) { + foreach ($callables as $callable) { + $callableReflector = new \ReflectionFunction($callable(...)); + if ($callableReflector->getNumberOfParameters() <= 2) { + $this->classAttributeConfigurators[$attributeName][] = $callable; + continue; + } - $reflectorParameter = $callableReflector->getParameters()[2]; - $parameterType = $reflectorParameter->getType(); - $types = []; - if ($parameterType instanceof \ReflectionUnionType) { - foreach ($parameterType->getTypes() as $type) { - $types[] = $type->getName(); + $reflectorParameter = $callableReflector->getParameters()[2]; + $parameterType = $reflectorParameter->getType(); + $types = []; + if ($parameterType instanceof \ReflectionUnionType) { + foreach ($parameterType->getTypes() as $type) { + $types[] = $type->getName(); + } + } elseif ($parameterType instanceof \ReflectionNamedType) { + $types[] = $parameterType->getName(); + } else { + throw new LogicException(\sprintf('Argument "$%s" of attribute autoconfigurator should have a type, use one or more of "\ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\Reflector" in "%s" on line "%d".', $reflectorParameter->getName(), $callableReflector->getFileName(), $callableReflector->getStartLine())); } - } elseif ($parameterType instanceof \ReflectionNamedType) { - $types[] = $parameterType->getName(); - } else { - throw new LogicException(\sprintf('Argument "$%s" of attribute autoconfigurator should have a type, use one or more of "\ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\Reflector" in "%s" on line "%d".', $reflectorParameter->getName(), $callableReflector->getFileName(), $callableReflector->getStartLine())); - } - try { - $attributeReflector = new \ReflectionClass($attributeName); - } catch (\ReflectionException) { - continue; - } + try { + $attributeReflector = new \ReflectionClass($attributeName); + } catch (\ReflectionException) { + continue; + } - $targets = $attributeReflector->getAttributes(\Attribute::class)[0] ?? 0; - $targets = $targets ? $targets->getArguments()[0] ?? -1 : 0; + $targets = $attributeReflector->getAttributes(\Attribute::class)[0] ?? 0; + $targets = $targets ? $targets->getArguments()[0] ?? -1 : 0; - foreach (['class', 'method', 'property', 'parameter'] as $symbol) { - if (['Reflector'] !== $types) { - if (!\in_array('Reflection'.ucfirst($symbol), $types, true)) { - continue; - } - if (!($targets & \constant('Attribute::TARGET_'.strtoupper($symbol)))) { - throw new LogicException(\sprintf('Invalid type "Reflection%s" on argument "$%s": attribute "%s" cannot target a '.$symbol.' in "%s" on line "%d".', ucfirst($symbol), $reflectorParameter->getName(), $attributeName, $callableReflector->getFileName(), $callableReflector->getStartLine())); + foreach (['class', 'method', 'property', 'parameter'] as $symbol) { + if (['Reflector'] !== $types) { + if (!\in_array('Reflection' . ucfirst($symbol), $types, true)) { + continue; + } + if (!($targets & \constant('Attribute::TARGET_' . strtoupper($symbol)))) { + throw new LogicException(\sprintf('Invalid type "Reflection%s" on argument "$%s": attribute "%s" cannot target a ' . $symbol . ' in "%s" on line "%d".', ucfirst($symbol), $reflectorParameter->getName(), $attributeName, $callableReflector->getFileName(), $callableReflector->getStartLine())); + } } + $this->{$symbol . 'AttributeConfigurators'}[$attributeName][] = $callable; } - $this->{$symbol.'AttributeConfigurators'}[$attributeName] = $callable; } } @@ -94,13 +96,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed $instanceof = $value->getInstanceofConditionals(); $conditionals = $instanceof[$classReflector->getName()] ?? new ChildDefinition(''); - if ($this->classAttributeConfigurators) { - foreach ($classReflector->getAttributes() as $attribute) { - if ($configurator = $this->findConfigurator($this->classAttributeConfigurators, $attribute->getName())) { - $configurator($conditionals, $attribute->newInstance(), $classReflector); - } - } - } + $this->callConfigurators($this->classAttributeConfigurators, $conditionals, $classReflector); if ($this->parameterAttributeConfigurators) { try { @@ -111,11 +107,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed if ($constructorReflector) { foreach ($constructorReflector->getParameters() as $parameterReflector) { - foreach ($parameterReflector->getAttributes() as $attribute) { - if ($configurator = $this->findConfigurator($this->parameterAttributeConfigurators, $attribute->getName())) { - $configurator($conditionals, $attribute->newInstance(), $parameterReflector); - } - } + $this->callConfigurators($this->parameterAttributeConfigurators, $conditionals, $parameterReflector); } } } @@ -126,22 +118,10 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed continue; } - if ($this->methodAttributeConfigurators) { - foreach ($methodReflector->getAttributes() as $attribute) { - if ($configurator = $this->findConfigurator($this->methodAttributeConfigurators, $attribute->getName())) { - $configurator($conditionals, $attribute->newInstance(), $methodReflector); - } - } - } + $this->callConfigurators($this->methodAttributeConfigurators, $conditionals, $methodReflector); - if ($this->parameterAttributeConfigurators) { - foreach ($methodReflector->getParameters() as $parameterReflector) { - foreach ($parameterReflector->getAttributes() as $attribute) { - if ($configurator = $this->findConfigurator($this->parameterAttributeConfigurators, $attribute->getName())) { - $configurator($conditionals, $attribute->newInstance(), $parameterReflector); - } - } - } + foreach ($methodReflector->getParameters() as $parameterReflector) { + $this->callConfigurators($this->parameterAttributeConfigurators, $conditionals, $parameterReflector); } } } @@ -152,11 +132,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed continue; } - foreach ($propertyReflector->getAttributes() as $attribute) { - if ($configurator = $this->findConfigurator($this->propertyAttributeConfigurators, $attribute->getName())) { - $configurator($conditionals, $attribute->newInstance(), $propertyReflector); - } - } + $this->callConfigurators($this->propertyAttributeConfigurators, $conditionals, $propertyReflector); } } @@ -168,19 +144,37 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed return parent::processValue($value, $isRoot); } + /** + * Call all the configurators for the given attribute. + * + * @param array $configurators + */ + private function callConfigurators(array &$configurators, ChildDefinition $conditionals, \ReflectionClass|\ReflectionMethod|\ReflectionParameter|\ReflectionProperty $reflector): void + { + if (!$configurators) { + return; + } + + foreach ($reflector->getAttributes() as $attribute) { + foreach ($this->findConfigurators($configurators, $attribute->getName()) as $configurator) { + $configurator($conditionals, $attribute->newInstance(), $reflector); + } + } + } + /** * Find the first configurator for the given attribute name, looking up the class hierarchy. */ - private function findConfigurator(array &$configurators, string $attributeName): ?callable + private function findConfigurators(array &$configurators, string $attributeName): array { if (\array_key_exists($attributeName, $configurators)) { return $configurators[$attributeName]; } if (class_exists($attributeName) && $parent = get_parent_class($attributeName)) { - return $configurators[$attributeName] = self::findConfigurator($configurators, $parent); + return $configurators[$attributeName] = $this->findConfigurators($configurators, $parent); } - return $configurators[$attributeName] = null; + return $configurators[$attributeName] = []; } } diff --git a/ContainerBuilder.php b/ContainerBuilder.php index 8197b104c..47202cf7d 100644 --- a/ContainerBuilder.php +++ b/ContainerBuilder.php @@ -129,7 +129,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface private array $autoconfiguredInstanceof = []; /** - * @var array + * @var array */ private array $autoconfiguredAttributes = []; @@ -717,12 +717,11 @@ public function merge(self $container): void $this->autoconfiguredInstanceof[$interface] = $childDefinition; } - foreach ($container->getAutoconfiguredAttributes() as $attribute => $configurator) { - if (isset($this->autoconfiguredAttributes[$attribute])) { - throw new InvalidArgumentException(\sprintf('"%s" has already been autoconfigured and merge() does not support merging autoconfiguration for the same attribute.', $attribute)); - } - - $this->autoconfiguredAttributes[$attribute] = $configurator; + foreach ($container->getAttributeAutoconfigurators() as $attribute => $configurators) { + $this->autoconfiguredAttributes[$attribute] = array_merge( + $this->autoconfiguredAttributes[$attribute] ?? [], + $configurators) + ; } } @@ -1448,7 +1447,7 @@ public function registerForAutoconfiguration(string $interface): ChildDefinition */ public function registerAttributeForAutoconfiguration(string $attributeClass, callable $configurator): void { - $this->autoconfiguredAttributes[$attributeClass] = $configurator; + $this->autoconfiguredAttributes[$attributeClass][] = $configurator; } /** @@ -1489,9 +1488,30 @@ public function getAutoconfiguredInstanceof(): array } /** - * @return array + * @return array + * + * @deprecated Use {@see getAttributeAutoconfigurators()} instead */ public function getAutoconfiguredAttributes(): array + { + trigger_deprecation('symfony/dependency-injection', '7.3', 'The "%s()" method is deprecated, use "getAttributeAutoconfigurators()" instead.', __METHOD__); + + $autoconfiguredAttributes = []; + foreach ($this->autoconfiguredAttributes as $attribute => $configurators) { + if (count($configurators) > 1) { + throw new LogicException(\sprintf('The "%s" attribute has %d configurators. Use "getAttributeAutoconfigurators()" to get all of them.', $attribute, count($configurators))); + } + + $autoconfiguredAttributes[$attribute] = $configurators[0]; + } + + return $autoconfiguredAttributes; + } + + /** + * @return array + */ + public function getAttributeAutoconfigurators(): array { return $this->autoconfiguredAttributes; } diff --git a/Tests/ContainerBuilderTest.php b/Tests/ContainerBuilderTest.php index ccae8c30e..774b1f88b 100644 --- a/Tests/ContainerBuilderTest.php +++ b/Tests/ContainerBuilderTest.php @@ -25,6 +25,7 @@ use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PassConfig; @@ -34,6 +35,7 @@ use Symfony\Component\DependencyInjection\Exception\BadMethodCallException; use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException; use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; @@ -829,6 +831,36 @@ public function testMergeThrowsExceptionForDuplicateAutomaticInstanceofDefinitio $container->merge($config); } + public function testMergeAttributeAutoconfiguration() + { + $container = new ContainerBuilder(); + $container->registerAttributeForAutoconfiguration(AsTaggedItem::class, $c1 = static function (Definition $definition) {}); + $config = new ContainerBuilder(); + $config->registerAttributeForAutoconfiguration(AsTaggedItem::class, $c2 = function (Definition $definition) {}); + + $container->merge($config); + $this->assertSame([AsTaggedItem::class => [$c1, $c2]], $container->getAttributeAutoconfigurators()); + } + + /** + * @group legacy + */ + public function testGetAutoconfiguredAttributes() + { + $container = new ContainerBuilder(); + $container->registerAttributeForAutoconfiguration(AsTaggedItem::class, $c = static function () {}); + + $this->expectUserDeprecationMessage('Since symfony/dependency-injection 7.3: The "Symfony\Component\DependencyInjection\ContainerBuilder::getAutoconfiguredAttributes()" method is deprecated, use "getAttributeAutoconfigurators()" instead.'); + $configurators = $container->getAutoconfiguredAttributes(); + $this->assertSame($c, $configurators[AsTaggedItem::class]); + + // Method call fails with more than one configurator for a given attribute + $container->registerAttributeForAutoconfiguration(AsTaggedItem::class, $c = static function () {}); + + $this->expectException(LogicException::class); + $container->getAutoconfiguredAttributes(); + } + public function testResolveEnvValues() { $_ENV['DUMMY_ENV_VAR'] = 'du%%y'; From cb416d889335566a59f65eef59ce0b0568ec96cd Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 28 Feb 2025 22:27:11 +0100 Subject: [PATCH 15/24] [VarExporter] Leverage native lazy objects --- LazyProxy/PhpDumper/LazyServiceDumper.php | 24 +++-- Tests/Dumper/PhpDumperTest.php | 6 +- ...y_autowire_attribute_with_intersection.php | 9 +- ...y_autowire_attribute_with_intersection.php | 94 +++++++++++++++++++ Tests/Fixtures/php/services_dedup_lazy.php | 15 ++- 5 files changed, 131 insertions(+), 17 deletions(-) create mode 100644 Tests/Fixtures/php/legacy_lazy_autowire_attribute_with_intersection.php diff --git a/LazyProxy/PhpDumper/LazyServiceDumper.php b/LazyProxy/PhpDumper/LazyServiceDumper.php index 60c6a4d60..0933c1a59 100644 --- a/LazyProxy/PhpDumper/LazyServiceDumper.php +++ b/LazyProxy/PhpDumper/LazyServiceDumper.php @@ -187,18 +187,28 @@ public function getProxyClass(Definition $definition, bool $asGhostObject, ?\Ref $class = 'object' !== $definition->getClass() ? $definition->getClass() : 'stdClass'; $class = new \ReflectionClass($class); - if (\PHP_VERSION_ID >= 80400) { - if ($asGhostObject) { - return $class->name; - } + if (\PHP_VERSION_ID < 80400) { + return preg_replace('/^.*\\\\/', '', $definition->getClass()) + .($asGhostObject ? 'Ghost' : 'Proxy') + .ucfirst(substr(hash('xxh128', $this->salt.'+'.$class->name.'+'.serialize($definition->getTag('proxy'))), -7)); + } + + if ($asGhostObject) { + return $class->name; + } + + if (!$definition->hasTag('proxy') && !$class->isInterface()) { + $parent = $class; + do { + $extendsInternalClass = $parent->isInternal(); + } while (!$extendsInternalClass && $parent = $parent->getParentClass()); - if (!$definition->hasTag('proxy') && !$class->isInterface()) { + if (!$extendsInternalClass) { return $class->name; } } - return preg_replace('/^.*\\\\/', '', $definition->getClass()) - .($asGhostObject ? 'Ghost' : 'Proxy') + return preg_replace('/^.*\\\\/', '', $definition->getClass()).'Proxy' .ucfirst(substr(hash('xxh128', $this->salt.'+'.$class->name.'+'.serialize($definition->getTag('proxy'))), -7)); } } diff --git a/Tests/Dumper/PhpDumperTest.php b/Tests/Dumper/PhpDumperTest.php index e4b5456dc..a117a69a0 100644 --- a/Tests/Dumper/PhpDumperTest.php +++ b/Tests/Dumper/PhpDumperTest.php @@ -2036,7 +2036,11 @@ public function testLazyAutowireAttributeWithIntersection() $dumper = new PhpDumper($container); - $this->assertStringEqualsFile(self::$fixturesPath.'/php/lazy_autowire_attribute_with_intersection.php', $dumper->dump()); + if (\PHP_VERSION_ID >= 80400) { + $this->assertStringEqualsFile(self::$fixturesPath.'/php/lazy_autowire_attribute_with_intersection.php', $dumper->dump()); + } else { + $this->assertStringEqualsFile(self::$fixturesPath.'/php/legacy_lazy_autowire_attribute_with_intersection.php', $dumper->dump()); + } } public function testCallableAdapterConsumer() diff --git a/Tests/Fixtures/php/lazy_autowire_attribute_with_intersection.php b/Tests/Fixtures/php/lazy_autowire_attribute_with_intersection.php index fcf66ad12..15cab6e1d 100644 --- a/Tests/Fixtures/php/lazy_autowire_attribute_with_intersection.php +++ b/Tests/Fixtures/php/lazy_autowire_attribute_with_intersection.php @@ -74,21 +74,16 @@ protected static function get_Lazy_Foo_QFdMZVKService($container, $lazyLoad = tr class objectProxy1fd6daa implements \Symfony\Component\DependencyInjection\Tests\Compiler\AInterface, \Symfony\Component\DependencyInjection\Tests\Compiler\IInterface, \Symfony\Component\VarExporter\LazyObjectInterface { - use \Symfony\Component\VarExporter\LazyProxyTrait; + use \Symfony\Component\VarExporter\Internal\LazyDecoratorTrait; private const LAZY_OBJECT_PROPERTY_SCOPES = []; public function initializeLazyObject(): \Symfony\Component\DependencyInjection\Tests\Compiler\AInterface&\Symfony\Component\DependencyInjection\Tests\Compiler\IInterface { - if ($state = $this->lazyObjectState ?? null) { - return $state->realInstance ??= ($state->initializer)(); - } - - return $this; + return $this->lazyObjectState->realInstance; } } // Help opcache.preload discover always-needed symbols class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); -class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/Tests/Fixtures/php/legacy_lazy_autowire_attribute_with_intersection.php b/Tests/Fixtures/php/legacy_lazy_autowire_attribute_with_intersection.php new file mode 100644 index 000000000..fcf66ad12 --- /dev/null +++ b/Tests/Fixtures/php/legacy_lazy_autowire_attribute_with_intersection.php @@ -0,0 +1,94 @@ +services = $this->privates = []; + $this->methodMap = [ + 'foo' => 'getFooService', + ]; + + $this->aliases = []; + } + + public function compile(): void + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + public function isCompiled(): bool + { + return true; + } + + protected function createProxy($class, \Closure $factory) + { + return $factory(); + } + + /** + * Gets the public 'foo' shared autowired service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Compiler\AAndIInterfaceConsumer + */ + protected static function getFooService($container) + { + $a = ($container->privates['.lazy.foo.qFdMZVK'] ?? self::get_Lazy_Foo_QFdMZVKService($container)); + + if (isset($container->services['foo'])) { + return $container->services['foo']; + } + + return $container->services['foo'] = new \Symfony\Component\DependencyInjection\Tests\Compiler\AAndIInterfaceConsumer($a); + } + + /** + * Gets the private '.lazy.foo.qFdMZVK' shared service. + * + * @return \object + */ + protected static function get_Lazy_Foo_QFdMZVKService($container, $lazyLoad = true) + { + if (true === $lazyLoad) { + return $container->privates['.lazy.foo.qFdMZVK'] = $container->createProxy('objectProxy1fd6daa', static fn () => \objectProxy1fd6daa::createLazyProxy(static fn () => self::get_Lazy_Foo_QFdMZVKService($container, false))); + } + + return ($container->services['foo'] ?? self::getFooService($container)); + } +} + +class objectProxy1fd6daa implements \Symfony\Component\DependencyInjection\Tests\Compiler\AInterface, \Symfony\Component\DependencyInjection\Tests\Compiler\IInterface, \Symfony\Component\VarExporter\LazyObjectInterface +{ + use \Symfony\Component\VarExporter\LazyProxyTrait; + + private const LAZY_OBJECT_PROPERTY_SCOPES = []; + + public function initializeLazyObject(): \Symfony\Component\DependencyInjection\Tests\Compiler\AInterface&\Symfony\Component\DependencyInjection\Tests\Compiler\IInterface + { + if ($state = $this->lazyObjectState ?? null) { + return $state->realInstance ??= ($state->initializer)(); + } + + return $this; + } +} + +// Help opcache.preload discover always-needed symbols +class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class); diff --git a/Tests/Fixtures/php/services_dedup_lazy.php b/Tests/Fixtures/php/services_dedup_lazy.php index 413a883e5..2c6142d0e 100644 --- a/Tests/Fixtures/php/services_dedup_lazy.php +++ b/Tests/Fixtures/php/services_dedup_lazy.php @@ -66,7 +66,7 @@ protected static function getBarService($container, $lazyLoad = true) protected static function getBazService($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->services['baz'] = new \ReflectionClass('stdClass')->newLazyProxy(static fn () => self::getBazService($container, false)); + return $container->services['baz'] = $container->createProxy('stdClassProxyAa01f12', static fn () => \stdClassProxyAa01f12::createLazyProxy(static fn () => self::getBazService($container, false))); } return \foo_bar(); @@ -80,7 +80,7 @@ protected static function getBazService($container, $lazyLoad = true) protected static function getBuzService($container, $lazyLoad = true) { if (true === $lazyLoad) { - return $container->services['buz'] = new \ReflectionClass('stdClass')->newLazyProxy(static fn () => self::getBuzService($container, false)); + return $container->services['buz'] = $container->createProxy('stdClassProxyAa01f12', static fn () => \stdClassProxyAa01f12::createLazyProxy(static fn () => self::getBuzService($container, false))); } return \foo_bar(); @@ -100,3 +100,14 @@ protected static function getFooService($container, $lazyLoad = true) return $lazyLoad; } } + +class stdClassProxyAa01f12 extends \stdClass implements \Symfony\Component\VarExporter\LazyObjectInterface +{ + use \Symfony\Component\VarExporter\Internal\LazyDecoratorTrait; + + private const LAZY_OBJECT_PROPERTY_SCOPES = []; +} + +// Help opcache.preload discover always-needed symbols +class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class); +class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class); From ea9789092af343ce78d02332ed0f41ab745b3728 Mon Sep 17 00:00:00 2001 From: Zuruuh Date: Wed, 9 Apr 2025 09:49:41 +0000 Subject: [PATCH 16/24] [DependencyInjection] Add "when" argument to #[AsAlias] --- Attribute/AsAlias.php | 12 ++++++++++-- CHANGELOG.md | 1 + Loader/FileLoader.php | 10 +++++++--- .../PrototypeAsAlias/WithAsAliasBothEnv.php | 10 ++++++++++ .../PrototypeAsAlias/WithAsAliasDevEnv.php | 10 ++++++++++ .../PrototypeAsAlias/WithAsAliasProdEnv.php | 10 ++++++++++ Tests/Loader/FileLoaderTest.php | 16 ++++++++++++++-- 7 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 Tests/Fixtures/PrototypeAsAlias/WithAsAliasBothEnv.php create mode 100644 Tests/Fixtures/PrototypeAsAlias/WithAsAliasDevEnv.php create mode 100644 Tests/Fixtures/PrototypeAsAlias/WithAsAliasProdEnv.php diff --git a/Attribute/AsAlias.php b/Attribute/AsAlias.php index 2f03e5fcd..0839afa48 100644 --- a/Attribute/AsAlias.php +++ b/Attribute/AsAlias.php @@ -20,12 +20,20 @@ final class AsAlias { /** - * @param string|null $id The id of the alias - * @param bool $public Whether to declare the alias public + * @var list + */ + public array $when = []; + + /** + * @param string|null $id The id of the alias + * @param bool $public Whether to declare the alias public + * @param string|list $when The environments under which the class will be registered as a service (i.e. "dev", "test", "prod") */ public function __construct( public ?string $id = null, public bool $public = false, + string|array $when = [], ) { + $this->when = (array) $when; } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 07521bc86..df3486a9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ CHANGELOG for auto-configuration of classes excluded from the service container * Accept multiple auto-configuration callbacks for the same attribute class * Leverage native lazy objects when possible for lazy services + * Add `when` argument to `#[AsAlias]` 7.2 --- diff --git a/Loader/FileLoader.php b/Loader/FileLoader.php index 9e17bc424..bc38767bc 100644 --- a/Loader/FileLoader.php +++ b/Loader/FileLoader.php @@ -224,10 +224,14 @@ public function registerClasses(Definition $prototype, string $namespace, string if (null === $alias) { throw new LogicException(\sprintf('Alias cannot be automatically determined for class "%s". If you have used the #[AsAlias] attribute with a class implementing multiple interfaces, add the interface you want to alias to the first parameter of #[AsAlias].', $class)); } - if (isset($this->aliases[$alias])) { - throw new LogicException(\sprintf('The "%s" alias has already been defined with the #[AsAlias] attribute in "%s".', $alias, $this->aliases[$alias])); + + if (!$attribute->when || \in_array($this->env, $attribute->when, true)) { + if (isset($this->aliases[$alias])) { + throw new LogicException(\sprintf('The "%s" alias has already been defined with the #[AsAlias] attribute in "%s".', $alias, $this->aliases[$alias])); + } + + $this->aliases[$alias] = new Alias($class, $public); } - $this->aliases[$alias] = new Alias($class, $public); } } diff --git a/Tests/Fixtures/PrototypeAsAlias/WithAsAliasBothEnv.php b/Tests/Fixtures/PrototypeAsAlias/WithAsAliasBothEnv.php new file mode 100644 index 000000000..252842be3 --- /dev/null +++ b/Tests/Fixtures/PrototypeAsAlias/WithAsAliasBothEnv.php @@ -0,0 +1,10 @@ +registerClasses( (new Definition())->setAutoconfigured(true), 'Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\\', @@ -374,6 +377,15 @@ public static function provideResourcesWithAsAliasAttributes(): iterable AliasBarInterface::class => new Alias(WithAsAliasIdMultipleInterface::class), AliasFooInterface::class => new Alias(WithAsAliasIdMultipleInterface::class), ]]; + yield 'Dev-env specific' => ['PrototypeAsAlias/WithAsAlias*Env.php', [ + AliasFooInterface::class => new Alias(WithAsAliasDevEnv::class), + AliasBarInterface::class => new Alias(WithAsAliasBothEnv::class), + ], 'dev']; + yield 'Prod-env specific' => ['PrototypeAsAlias/WithAsAlias*Env.php', [ + AliasFooInterface::class => new Alias(WithAsAliasProdEnv::class), + AliasBarInterface::class => new Alias(WithAsAliasBothEnv::class), + ], 'prod']; + yield 'Test-env specific' => ['PrototypeAsAlias/WithAsAlias*Env.php', [], 'test']; } /** From efbe665226911a403894d5a0974b4a76cb63547b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Mon, 31 Mar 2025 16:11:11 +0200 Subject: [PATCH 17/24] [DependencyInjection] Add better return type on ContainerInterface::get() --- ContainerInterface.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ContainerInterface.php b/ContainerInterface.php index 39fd080c3..6d6f6d3bf 100644 --- a/ContainerInterface.php +++ b/ContainerInterface.php @@ -33,11 +33,13 @@ interface ContainerInterface extends PsrContainerInterface public function set(string $id, ?object $service): void; /** + * @template C of object * @template B of self::*_REFERENCE * - * @param B $invalidBehavior + * @param string|class-string $id + * @param B $invalidBehavior * - * @psalm-return (B is self::EXCEPTION_ON_INVALID_REFERENCE|self::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE ? object : object|null) + * @return ($id is class-string ? (B is 0|1 ? C|object : C|object|null) : (B is 0|1 ? object : object|null)) * * @throws ServiceCircularReferenceException When a circular reference is detected * @throws ServiceNotFoundException When the service is not defined From 01e3ad793441e2f21f1fa7ce6ba1f102dd1f144c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 27 Apr 2025 15:39:08 +0200 Subject: [PATCH 18/24] Remove unneeded use statements --- Tests/Loader/FileLoaderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Loader/FileLoaderTest.php b/Tests/Loader/FileLoaderTest.php index 2b57e8ef7..0ad1b363c 100644 --- a/Tests/Loader/FileLoaderTest.php +++ b/Tests/Loader/FileLoaderTest.php @@ -26,7 +26,6 @@ use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAliasBothEnv; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\BadClasses\MissingParent; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\FooInterface; @@ -40,6 +39,7 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\AliasBarInterface; use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\AliasFooInterface; use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAlias; +use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAliasBothEnv; use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAliasDevEnv; use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAliasIdMultipleInterface; use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAliasInterface; From 5a6f679ac38f2bf1298647f6b208e7acf8877a2d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 9 May 2025 10:05:11 +0200 Subject: [PATCH 19/24] [DependencyInjection][FrameworkBundle] Fix precedence of App\Kernel alias and ignore container.excluded tag on synthetic services --- Compiler/ResolveInstanceofConditionalsPass.php | 7 ++++++- .../ResolveInstanceofConditionalsPassTest.php | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Compiler/ResolveInstanceofConditionalsPass.php b/Compiler/ResolveInstanceofConditionalsPass.php index 90d4569c4..52dc56c0f 100644 --- a/Compiler/ResolveInstanceofConditionalsPass.php +++ b/Compiler/ResolveInstanceofConditionalsPass.php @@ -112,8 +112,8 @@ private function processDefinition(ContainerBuilder $container, string $id, Defi $definition = substr_replace($definition, '53', 2, 2); $definition = substr_replace($definition, 'Child', 44, 0); } - /** @var ChildDefinition $definition */ $definition = unserialize($definition); + /** @var ChildDefinition $definition */ $definition->setParent($parent); if (null !== $shared && !isset($definition->getChanges()['shared'])) { @@ -149,6 +149,11 @@ private function processDefinition(ContainerBuilder $container, string $id, Defi ->setAbstract(true); } + if ($definition->isSynthetic()) { + // Ignore container.excluded tag on synthetic services + $definition->clearTag('container.excluded'); + } + return $definition; } diff --git a/Tests/Compiler/ResolveInstanceofConditionalsPassTest.php b/Tests/Compiler/ResolveInstanceofConditionalsPassTest.php index 76143fc9b..b4e50d39f 100644 --- a/Tests/Compiler/ResolveInstanceofConditionalsPassTest.php +++ b/Tests/Compiler/ResolveInstanceofConditionalsPassTest.php @@ -376,6 +376,21 @@ public function testDecoratorsKeepBehaviorDescribingTags() ], $container->getDefinition('decorator')->getTags()); $this->assertFalse($container->hasParameter('container.behavior_describing_tags')); } + + public function testSyntheticService() + { + $container = new ContainerBuilder(); + $container->register('kernel', \stdClass::class) + ->setInstanceofConditionals([ + \stdClass::class => (new ChildDefinition('')) + ->addTag('container.excluded'), + ]) + ->setSynthetic(true); + + (new ResolveInstanceofConditionalsPass())->process($container); + + $this->assertSame([], $container->getDefinition('kernel')->getTags()); + } } class DecoratorWithBehavior implements ResetInterface, ResourceCheckerInterface, ServiceSubscriberInterface From 42cb5ab4d4453bc4e04b123c57ec64c228d4e689 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 15 May 2025 15:18:10 +0200 Subject: [PATCH 20/24] [DependencyInjection] Fix missing binding for ServiceCollectionInterface when declaring a service subscriber --- Compiler/RegisterServiceSubscribersPass.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Compiler/RegisterServiceSubscribersPass.php b/Compiler/RegisterServiceSubscribersPass.php index 87470c398..89b822bc5 100644 --- a/Compiler/RegisterServiceSubscribersPass.php +++ b/Compiler/RegisterServiceSubscribersPass.php @@ -20,6 +20,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Contracts\Service\Attribute\SubscribedService; +use Symfony\Contracts\Service\ServiceCollectionInterface; use Symfony\Contracts\Service\ServiceProviderInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface; @@ -134,6 +135,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed $value->setBindings([ PsrContainerInterface::class => new BoundArgument($locatorRef, false), ServiceProviderInterface::class => new BoundArgument($locatorRef, false), + ServiceCollectionInterface::class => new BoundArgument($locatorRef, false), ] + $value->getBindings()); return parent::processValue($value); From 8cb11f833d1f5bfbb2df97dfc23c92b4d42c18d9 Mon Sep 17 00:00:00 2001 From: matlec Date: Wed, 14 May 2025 19:55:19 +0200 Subject: [PATCH 21/24] [DependencyInjection] Make `DefinitionErrorExceptionPass` consider `IGNORE_ON_UNINITIALIZED_REFERENCE` and `RUNTIME_EXCEPTION_ON_INVALID_REFERENCE` the same --- Compiler/DefinitionErrorExceptionPass.php | 5 ++++- Tests/Compiler/DefinitionErrorExceptionPassTest.php | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Compiler/DefinitionErrorExceptionPass.php b/Compiler/DefinitionErrorExceptionPass.php index b6a2cf907..204401cd2 100644 --- a/Compiler/DefinitionErrorExceptionPass.php +++ b/Compiler/DefinitionErrorExceptionPass.php @@ -65,7 +65,10 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed } if ($value instanceof Reference && $this->currentId !== $targetId = (string) $value) { - if (ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()) { + if ( + ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior() + || ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $value->getInvalidBehavior() + ) { $this->sourceReferences[$targetId][$this->currentId] ??= true; } else { $this->sourceReferences[$targetId][$this->currentId] = false; diff --git a/Tests/Compiler/DefinitionErrorExceptionPassTest.php b/Tests/Compiler/DefinitionErrorExceptionPassTest.php index 9ab5c27fc..5ed7be315 100644 --- a/Tests/Compiler/DefinitionErrorExceptionPassTest.php +++ b/Tests/Compiler/DefinitionErrorExceptionPassTest.php @@ -64,6 +64,9 @@ public function testSkipNestedErrors() $container->register('foo', 'stdClass') ->addArgument(new Reference('bar', ContainerBuilder::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE)); + $container->register('baz', 'stdClass') + ->addArgument(new Reference('bar', ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE)); + $pass = new DefinitionErrorExceptionPass(); $pass->process($container); From aeb34cceee832d6265c36f1832e90ca99cd84cd2 Mon Sep 17 00:00:00 2001 From: matlec Date: Thu, 29 May 2025 10:10:20 +0200 Subject: [PATCH 22/24] [DependencyInjection] Make `YamlDumper` quote resolved env vars if necessary --- Dumper/YamlDumper.php | 16 +++++++-------- Tests/Dumper/YamlDumperTest.php | 20 +++++++++++++++++++ .../yaml/container_with_env_placeholders.yml | 19 ++++++++++++++++++ 3 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 Tests/Fixtures/yaml/container_with_env_placeholders.yml diff --git a/Dumper/YamlDumper.php b/Dumper/YamlDumper.php index 6b72aff14..299558039 100644 --- a/Dumper/YamlDumper.php +++ b/Dumper/YamlDumper.php @@ -50,18 +50,18 @@ public function dump(array $options = []): string $this->dumper ??= new YmlDumper(); - return $this->container->resolveEnvPlaceholders($this->addParameters()."\n".$this->addServices()); + return $this->addParameters()."\n".$this->addServices(); } private function addService(string $id, Definition $definition): string { - $code = " $id:\n"; + $code = " {$this->dumper->dump($id)}:\n"; if ($class = $definition->getClass()) { if (str_starts_with($class, '\\')) { $class = substr($class, 1); } - $code .= sprintf(" class: %s\n", $this->dumper->dump($class)); + $code .= sprintf(" class: %s\n", $this->dumper->dump($this->container->resolveEnvPlaceholders($class))); } if (!$definition->isPrivate()) { @@ -87,7 +87,7 @@ private function addService(string $id, Definition $definition): string } if ($definition->getFile()) { - $code .= sprintf(" file: %s\n", $this->dumper->dump($definition->getFile())); + $code .= sprintf(" file: %s\n", $this->dumper->dump($this->container->resolveEnvPlaceholders($definition->getFile()))); } if ($definition->isSynthetic()) { @@ -238,7 +238,7 @@ private function dumpCallable(mixed $callable): mixed } } - return $callable; + return $this->container->resolveEnvPlaceholders($callable); } /** @@ -299,7 +299,7 @@ private function dumpValue(mixed $value): mixed if (\is_array($value)) { $code = []; foreach ($value as $k => $v) { - $code[$k] = $this->dumpValue($v); + $code[$this->container->resolveEnvPlaceholders($k)] = $this->dumpValue($v); } return $code; @@ -319,7 +319,7 @@ private function dumpValue(mixed $value): mixed throw new RuntimeException(sprintf('Unable to dump a service container if a parameter is an object or a resource, got "%s".', get_debug_type($value))); } - return $value; + return $this->container->resolveEnvPlaceholders($value); } private function getServiceCall(string $id, ?Reference $reference = null): string @@ -359,7 +359,7 @@ private function prepareParameters(array $parameters, bool $escape = true): arra $filtered[$key] = $value; } - return $escape ? $this->escape($filtered) : $filtered; + return $escape ? $this->container->resolveEnvPlaceholders($this->escape($filtered)) : $filtered; } private function escape(array $arguments): array diff --git a/Tests/Dumper/YamlDumperTest.php b/Tests/Dumper/YamlDumperTest.php index f9ff3fff7..3a21d7aa9 100644 --- a/Tests/Dumper/YamlDumperTest.php +++ b/Tests/Dumper/YamlDumperTest.php @@ -215,6 +215,26 @@ public function testDumpNonScalarTags() $this->assertEquals(file_get_contents(self::$fixturesPath.'/yaml/services_with_array_tags.yml'), $dumper->dump()); } + public function testDumpResolvedEnvPlaceholders() + { + $container = new ContainerBuilder(); + $container->setParameter('%env(PARAMETER_NAME)%', '%env(PARAMETER_VALUE)%'); + $container + ->register('service', '%env(SERVICE_CLASS)%') + ->setFile('%env(SERVICE_FILE)%') + ->addArgument('%env(SERVICE_ARGUMENT)%') + ->setProperty('%env(SERVICE_PROPERTY_NAME)%', '%env(SERVICE_PROPERTY_VALUE)%') + ->addMethodCall('%env(SERVICE_METHOD_NAME)%', ['%env(SERVICE_METHOD_ARGUMENT)%']) + ->setFactory('%env(SERVICE_FACTORY)%') + ->setConfigurator('%env(SERVICE_CONFIGURATOR)%') + ->setPublic(true) + ; + $container->compile(); + $dumper = new YamlDumper($container); + + $this->assertEquals(file_get_contents(self::$fixturesPath.'/yaml/container_with_env_placeholders.yml'), $dumper->dump()); + } + private function assertEqualYamlStructure(string $expected, string $yaml, string $message = '') { $parser = new Parser(); diff --git a/Tests/Fixtures/yaml/container_with_env_placeholders.yml b/Tests/Fixtures/yaml/container_with_env_placeholders.yml new file mode 100644 index 000000000..46c91130f --- /dev/null +++ b/Tests/Fixtures/yaml/container_with_env_placeholders.yml @@ -0,0 +1,19 @@ +parameters: + '%env(PARAMETER_NAME)%': '%env(PARAMETER_VALUE)%' + +services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + public: true + synthetic: true + service: + class: '%env(SERVICE_CLASS)%' + public: true + file: '%env(SERVICE_FILE)%' + arguments: ['%env(SERVICE_ARGUMENT)%'] + properties: { '%env(SERVICE_PROPERTY_NAME)%': '%env(SERVICE_PROPERTY_VALUE)%' } + calls: + - ['%env(SERVICE_METHOD_NAME)%', ['%env(SERVICE_METHOD_ARGUMENT)%']] + + factory: '%env(SERVICE_FACTORY)%' + configurator: '%env(SERVICE_CONFIGURATOR)%' From 4d9405624d65eaa969bab772a976f13e18ec163e Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Fri, 6 Jun 2025 09:38:13 -0500 Subject: [PATCH 23/24] Improve docblock on compile() --- ContainerBuilder.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ContainerBuilder.php b/ContainerBuilder.php index 5be5b76f5..2771defe4 100644 --- a/ContainerBuilder.php +++ b/ContainerBuilder.php @@ -742,10 +742,11 @@ public function deprecateParameter(string $name, string $package, string $versio * * The parameter bag is frozen; * * Extension loading is disabled. * - * @param bool $resolveEnvPlaceholders Whether %env()% parameters should be resolved using the current - * env vars or be replaced by uniquely identifiable placeholders. - * Set to "true" when you want to use the current ContainerBuilder - * directly, keep to "false" when the container is dumped instead. + * @param bool $resolveEnvPlaceholders Whether %env()% parameters should be resolved at build time using + * the current env var values (true), or be resolved at runtime based + * on the environment (false). In general, this should be set to "true" + * when you want to use the current ContainerBuilder directly, and to + * "false" when the container is dumped instead. * * @return void */ From a7535bb7b26c466a4817c6c5990ba30ecbee266c Mon Sep 17 00:00:00 2001 From: matlec Date: Wed, 4 Jun 2025 15:43:26 +0200 Subject: [PATCH 24/24] [DependencyInjection] Fix `ServiceLocatorTagPass` indexes handling --- Attribute/AsTaggedItem.php | 4 +- Compiler/PriorityTaggedServiceTrait.php | 3 +- Compiler/ServiceLocatorTagPass.php | 69 ++++++++++---------- Tests/Compiler/ServiceLocatorTagPassTest.php | 63 +++++++++++++++++- 4 files changed, 100 insertions(+), 39 deletions(-) diff --git a/Attribute/AsTaggedItem.php b/Attribute/AsTaggedItem.php index 2e649bdea..6b1a94dd3 100644 --- a/Attribute/AsTaggedItem.php +++ b/Attribute/AsTaggedItem.php @@ -20,8 +20,8 @@ class AsTaggedItem { /** - * @param string|null $index The property or method to use to index the item in the locator - * @param int|null $priority The priority of the item; the higher the number, the earlier the tagged service will be located in the locator + * @param string|null $index The property or method to use to index the item in the iterator/locator + * @param int|null $priority The priority of the item; the higher the number, the earlier the tagged service will be located in the iterator/locator */ public function __construct( public ?string $index = null, diff --git a/Compiler/PriorityTaggedServiceTrait.php b/Compiler/PriorityTaggedServiceTrait.php index 77a1d7ef8..e3a4eba27 100644 --- a/Compiler/PriorityTaggedServiceTrait.php +++ b/Compiler/PriorityTaggedServiceTrait.php @@ -87,8 +87,7 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam if (null === $index && null === $defaultIndex && $defaultPriorityMethod && $class) { $defaultIndex = PriorityTaggedServiceUtil::getDefault($container, $serviceId, $class, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute, $checkTaggedItem); } - $decorated = $definition->getTag('container.decorator')[0]['id'] ?? null; - $index = $index ?? $defaultIndex ?? $defaultIndex = $decorated ?? $serviceId; + $index ??= $defaultIndex ??= $definition->getTag('container.decorator')[0]['id'] ?? $serviceId; $services[] = [$priority, ++$i, $index, $serviceId, $class]; } diff --git a/Compiler/ServiceLocatorTagPass.php b/Compiler/ServiceLocatorTagPass.php index 81c14ac5c..eedc0f484 100644 --- a/Compiler/ServiceLocatorTagPass.php +++ b/Compiler/ServiceLocatorTagPass.php @@ -54,17 +54,41 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed $value->setClass(ServiceLocator::class); } - $services = $value->getArguments()[0] ?? null; + $values = $value->getArguments()[0] ?? null; + $services = []; - if ($services instanceof TaggedIteratorArgument) { - $services = $this->findAndSortTaggedServices($services, $this->container); - } - - if (!\is_array($services)) { + if ($values instanceof TaggedIteratorArgument) { + foreach ($this->findAndSortTaggedServices($values, $this->container) as $k => $v) { + $services[$k] = new ServiceClosureArgument($v); + } + } elseif (!\is_array($values)) { throw new InvalidArgumentException(\sprintf('Invalid definition for service "%s": an array of references is expected as first argument when the "container.service_locator" tag is set.', $this->currentId)); + } else { + $i = 0; + + foreach ($values as $k => $v) { + if ($v instanceof ServiceClosureArgument) { + $services[$k] = $v; + continue; + } + + if ($i === $k) { + if ($v instanceof Reference) { + $k = (string) $v; + } + ++$i; + } elseif (\is_int($k)) { + $i = null; + } + + $services[$k] = new ServiceClosureArgument($v); + } + if (\count($services) === $i) { + ksort($services); + } } - $value->setArgument(0, self::map($services)); + $value->setArgument(0, $services); $id = '.service_locator.'.ContainerBuilder::hash($value); @@ -83,8 +107,12 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed public static function register(ContainerBuilder $container, array $map, ?string $callerId = null): Reference { + foreach ($map as $k => $v) { + $map[$k] = new ServiceClosureArgument($v); + } + $locator = (new Definition(ServiceLocator::class)) - ->addArgument(self::map($map)) + ->addArgument($map) ->addTag('container.service_locator'); if (null !== $callerId && $container->hasDefinition($callerId)) { @@ -109,29 +137,4 @@ public static function register(ContainerBuilder $container, array $map, ?string return new Reference($id); } - - public static function map(array $services): array - { - $i = 0; - - foreach ($services as $k => $v) { - if ($v instanceof ServiceClosureArgument) { - continue; - } - - if ($i === $k) { - if ($v instanceof Reference) { - unset($services[$k]); - $k = (string) $v; - } - ++$i; - } elseif (\is_int($k)) { - $i = null; - } - - $services[$k] = new ServiceClosureArgument($v); - } - - return $services; - } } diff --git a/Tests/Compiler/ServiceLocatorTagPassTest.php b/Tests/Compiler/ServiceLocatorTagPassTest.php index 812b47c7a..9a9306775 100644 --- a/Tests/Compiler/ServiceLocatorTagPassTest.php +++ b/Tests/Compiler/ServiceLocatorTagPassTest.php @@ -86,6 +86,26 @@ public function testProcessValue() $this->assertSame(CustomDefinition::class, \get_class($locator('inlines.service'))); } + public function testServiceListIsOrdered() + { + $container = new ContainerBuilder(); + + $container->register('bar', CustomDefinition::class); + $container->register('baz', CustomDefinition::class); + + $container->register('foo', ServiceLocator::class) + ->setArguments([[ + new Reference('baz'), + new Reference('bar'), + ]]) + ->addTag('container.service_locator') + ; + + (new ServiceLocatorTagPass())->process($container); + + $this->assertSame(['bar', 'baz'], array_keys($container->getDefinition('foo')->getArgument(0))); + } + public function testServiceWithKeyOverwritesPreviousInheritedKey() { $container = new ContainerBuilder(); @@ -170,6 +190,27 @@ public function testTaggedServices() $this->assertSame(TestDefinition2::class, $locator('baz')::class); } + public function testTaggedServicesKeysAreKept() + { + $container = new ContainerBuilder(); + + $container->register('bar', TestDefinition1::class)->addTag('test_tag', ['index' => 0]); + $container->register('baz', TestDefinition2::class)->addTag('test_tag', ['index' => 1]); + + $container->register('foo', ServiceLocator::class) + ->setArguments([new TaggedIteratorArgument('test_tag', 'index', null, true)]) + ->addTag('container.service_locator') + ; + + (new ServiceLocatorTagPass())->process($container); + + /** @var ServiceLocator $locator */ + $locator = $container->get('foo'); + + $this->assertSame(TestDefinition1::class, $locator(0)::class); + $this->assertSame(TestDefinition2::class, $locator(1)::class); + } + public function testIndexedByServiceIdWithDecoration() { $container = new ContainerBuilder(); @@ -201,15 +242,33 @@ public function testIndexedByServiceIdWithDecoration() static::assertInstanceOf(DecoratedService::class, $locator->get(Service::class)); } - public function testDefinitionOrderIsTheSame() + public function testServicesKeysAreKept() { $container = new ContainerBuilder(); $container->register('service-1'); $container->register('service-2'); + $container->register('service-3'); $locator = ServiceLocatorTagPass::register($container, [ - new Reference('service-2'), new Reference('service-1'), + 'service-2' => new Reference('service-2'), + 'foo' => new Reference('service-3'), + ]); + $locator = $container->getDefinition($locator); + $factories = $locator->getArguments()[0]; + + static::assertSame([0, 'service-2', 'foo'], array_keys($factories)); + } + + public function testDefinitionOrderIsTheSame() + { + $container = new ContainerBuilder(); + $container->register('service-1'); + $container->register('service-2'); + + $locator = ServiceLocatorTagPass::register($container, [ + 'service-2' => new Reference('service-2'), + 'service-1' => new Reference('service-1'), ]); $locator = $container->getDefinition($locator); $factories = $locator->getArguments()[0];