diff --git a/Argument/LazyClosure.php b/Argument/LazyClosure.php index 7a677ebbd..3e8718643 100644 --- a/Argument/LazyClosure.php +++ b/Argument/LazyClosure.php @@ -40,22 +40,22 @@ public function __get(mixed $name): mixed } if (isset($this->initializer)) { - $this->service = ($this->initializer)(); + if (\is_string($service = ($this->initializer)())) { + $service = (new \ReflectionClass($service))->newInstanceWithoutConstructor(); + } + $this->service = $service; unset($this->initializer); } return $this->service; } - public static function getCode(string $initializer, array $callable, Definition $definition, ContainerBuilder $container, ?string $id): string + public static function getCode(string $initializer, array $callable, string $class, ContainerBuilder $container, ?string $id): string { $method = $callable[1]; - $asClosure = 'Closure' === ($definition->getClass() ?: 'Closure'); - if ($asClosure) { + if ($asClosure = 'Closure' === $class) { $class = ($callable[0] instanceof Reference ? $container->findDefinition($callable[0]) : $callable[0])->getClass(); - } else { - $class = $definition->getClass(); } $r = $container->getReflectionClass($class); 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/InlineServiceDefinitionsPass.php b/Compiler/InlineServiceDefinitionsPass.php index de4acb258..52af43f60 100644 --- a/Compiler/InlineServiceDefinitionsPass.php +++ b/Compiler/InlineServiceDefinitionsPass.php @@ -69,6 +69,9 @@ public function process(ContainerBuilder $container): void if (!$this->graph->hasNode($id)) { continue; } + if ($definition->isPublic()) { + $this->connectedIds[$id] = true; + } foreach ($this->graph->getNode($id)->getOutEdges() as $edge) { if (isset($notInlinedIds[$edge->getSourceNode()->getId()])) { $this->currentId = $id; @@ -188,17 +191,13 @@ private function isInlineableDefinition(string $id, Definition $definition): boo return true; } - if ($definition->isPublic()) { + if ($definition->isPublic() + || $this->currentId === $id + || !$this->graph->hasNode($id) + ) { return false; } - if (!$this->graph->hasNode($id)) { - return true; - } - - if ($this->currentId === $id) { - return false; - } $this->connectedIds[$id] = true; $srcIds = []; 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/ContainerBuilder.php b/ContainerBuilder.php index 7389ca631..e978cac41 100644 --- a/ContainerBuilder.php +++ b/ContainerBuilder.php @@ -791,10 +791,11 @@ public function parameterCannotBeEmpty(string $name, string $message): void * * 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. */ public function compile(bool $resolveEnvPlaceholders = false): void { @@ -1109,14 +1110,15 @@ private function createService(Definition $definition, array &$inlineServices, b } if (\is_array($callable) && ( - $callable[0] instanceof Reference + 'Closure' !== $class + || $callable[0] instanceof Reference || $callable[0] instanceof Definition && !isset($inlineServices[spl_object_hash($callable[0])]) )) { $initializer = function () use ($callable, &$inlineServices) { return $this->doResolveServices($callable[0], $inlineServices); }; - $proxy = eval('return '.LazyClosure::getCode('$initializer', $callable, $definition, $this, $id).';'); + $proxy = eval('return '.LazyClosure::getCode('$initializer', $callable, $class, $this, $id).';'); $this->shareService($definition, $proxy, $id, $inlineServices); return $proxy; diff --git a/Dumper/PhpDumper.php b/Dumper/PhpDumper.php index ee7e519a0..9568ad26b 100644 --- a/Dumper/PhpDumper.php +++ b/Dumper/PhpDumper.php @@ -1184,13 +1184,13 @@ private function addNewInstance(Definition $definition, string $return = '', ?st throw new RuntimeException(\sprintf('Cannot dump definition because of invalid factory method (%s).', $callable[1] ?: 'n/a')); } - if (['...'] === $arguments && ($definition->isLazy() || 'Closure' !== ($definition->getClass() ?? 'Closure')) && ( + if (['...'] === $arguments && ('Closure' !== ($class = $definition->getClass() ?: 'Closure') || $definition->isLazy() && ( $callable[0] instanceof Reference || ($callable[0] instanceof Definition && !$this->definitionVariables->contains($callable[0])) - )) { + ))) { $initializer = 'fn () => '.$this->dumpValue($callable[0]); - return $return.LazyClosure::getCode($initializer, $callable, $definition, $this->container, $id).$tail; + return $return.LazyClosure::getCode($initializer, $callable, $class, $this->container, $id).$tail; } if ($callable[0] instanceof Reference diff --git a/Dumper/YamlDumper.php b/Dumper/YamlDumper.php index ec115500b..d79e7b904 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/Argument/LazyClosureTest.php b/Tests/Argument/LazyClosureTest.php index 46ef15917..9652a86fd 100644 --- a/Tests/Argument/LazyClosureTest.php +++ b/Tests/Argument/LazyClosureTest.php @@ -34,7 +34,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'], LazyClosureTest::class, new ContainerBuilder(), 'foo'); } public function testThrowsOnNonFunctionalInterface() @@ -42,7 +42,7 @@ public function testThrowsOnNonFunctionalInterface() $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Cannot create adapter for service "foo" because interface "Symfony\Component\DependencyInjection\Tests\Argument\NonFunctionalInterface" doesn\'t have exactly one method.'); - LazyClosure::getCode('foo', [new \stdClass(), 'bar'], new Definition(NonFunctionalInterface::class), new ContainerBuilder(), 'foo'); + LazyClosure::getCode('foo', [new \stdClass(), 'bar'], NonFunctionalInterface::class, new ContainerBuilder(), 'foo'); } public function testThrowsOnUnknownMethodInInterface() @@ -50,7 +50,7 @@ public function testThrowsOnUnknownMethodInInterface() $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Cannot create lazy closure for service "bar" because its corresponding callable is invalid.'); - LazyClosure::getCode('bar', [new Definition(FunctionalInterface::class), 'bar'], new Definition(\Closure::class), new ContainerBuilder(), 'bar'); + LazyClosure::getCode('bar', [new Definition(FunctionalInterface::class), 'bar'], \Closure::class, new ContainerBuilder(), 'bar'); } } 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]; diff --git a/Tests/ContainerBuilderTest.php b/Tests/ContainerBuilderTest.php index 544303bbe..73be8aaed 100644 --- a/Tests/ContainerBuilderTest.php +++ b/Tests/ContainerBuilderTest.php @@ -48,6 +48,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\DependencyInjection\Tests\Compiler\Foo; +use Symfony\Component\DependencyInjection\Tests\Compiler\MyCallable; use Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface; use Symfony\Component\DependencyInjection\Tests\Compiler\Wither; use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; @@ -530,6 +531,19 @@ public function testClosureProxy() $this->assertInstanceOf(Foo::class, $container->get('closure_proxy')->theMethod()); } + public function testClosureProxyWithStaticMethod() + { + $container = new ContainerBuilder(); + $container->register('closure_proxy', SingleMethodInterface::class) + ->setPublic('true') + ->setFactory(['Closure', 'fromCallable']) + ->setArguments([[MyCallable::class, 'theMethodImpl']]); + $container->compile(); + + $this->assertInstanceOf(SingleMethodInterface::class, $container->get('closure_proxy')); + $this->assertSame(124, $container->get('closure_proxy')->theMethod()); + } + public function testCreateServiceClass() { $builder = new ContainerBuilder(); 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/StaticConstructorAutoconfigure.php b/Tests/Fixtures/StaticConstructorAutoconfigure.php index 3d42a8c77..09479fe55 100644 --- a/Tests/Fixtures/StaticConstructorAutoconfigure.php +++ b/Tests/Fixtures/StaticConstructorAutoconfigure.php @@ -12,7 +12,6 @@ namespace Symfony\Component\DependencyInjection\Tests\Fixtures; use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; -use Symfony\Component\DependencyInjection\Attribute\Factory; #[Autoconfigure(bind: ['$foo' => 'foo'], constructor: 'create')] class StaticConstructorAutoconfigure diff --git a/Tests/Fixtures/includes/autowiring_classes.php b/Tests/Fixtures/includes/autowiring_classes.php index 7349cb1a0..98cbbec90 100644 --- a/Tests/Fixtures/includes/autowiring_classes.php +++ b/Tests/Fixtures/includes/autowiring_classes.php @@ -461,6 +461,11 @@ class MyCallable public function __invoke(): void { } + + public static function theMethodImpl(): int + { + return 124; + } } class MyInlineService diff --git a/Tests/Fixtures/php/callable_adapter_consumer.php b/Tests/Fixtures/php/callable_adapter_consumer.php index 216dca434..ccd8d2e0b 100644 --- a/Tests/Fixtures/php/callable_adapter_consumer.php +++ b/Tests/Fixtures/php/callable_adapter_consumer.php @@ -50,6 +50,6 @@ public function getRemovedIds(): array */ protected static function getBarService($container) { - return $container->services['bar'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\CallableAdapterConsumer(new class(fn () => (new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo())) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface { public function theMethod() { return $this->service->cloneFoo(...\func_get_args()); } }); + return $container->services['bar'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\CallableAdapterConsumer(new class(fn () => new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo()) extends \Symfony\Component\DependencyInjection\Argument\LazyClosure implements \Symfony\Component\DependencyInjection\Tests\Compiler\SingleMethodInterface { public function theMethod() { return $this->service->cloneFoo(...\func_get_args()); } }); } } 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)%'
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: