diff --git a/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php b/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php index 7b001352ac8bd..6324f021afadf 100644 --- a/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php +++ b/src/Symfony/Component/DependencyInjection/Argument/LazyClosure.php @@ -11,8 +11,11 @@ namespace Symfony\Component\DependencyInjection\Argument; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\VarExporter\ProxyHelper; /** @@ -44,17 +47,43 @@ public function __get(mixed $name): mixed return $this->service; } - public static function getCode(string $initializer, ?\ReflectionClass $r, string $method, ?string $id): string + public static function getCode(string $initializer, array $callable, Definition $definition, ContainerBuilder $container, ?string $id): string { - if (!$r || !$r->hasMethod($method)) { + $method = $callable[1]; + $asClosure = 'Closure' === ($definition->getClass() ?: 'Closure'); + + if ($asClosure) { + $class = ($callable[0] instanceof Reference ? $container->findDefinition($callable[0]) : $callable[0])->getClass(); + } else { + $class = $definition->getClass(); + } + + $r = $container->getReflectionClass($class); + + if (!$asClosure) { + if (!$r || !$r->isInterface()) { + throw new RuntimeException(sprintf('Cannot create adapter for service "%s" because "%s" is not an interface.', $id, $class)); + } + if (1 !== \count($method = $r->getMethods())) { + throw new RuntimeException(sprintf('Cannot create adapter for service "%s" because interface "%s" doesn\'t have exactly one method.', $id, $class)); + } + $method = $method[0]->name; + } elseif (!$r || !$r->hasMethod($method)) { throw new RuntimeException(sprintf('Cannot create lazy closure for service "%s" because its corresponding callable is invalid.', $id)); } - $signature = ProxyHelper::exportSignature($r->getMethod($method)); - $signature = preg_replace('/: static$/', ': \\'.$r->name, $signature); + $code = ProxyHelper::exportSignature($r->getMethod($method)); + + if ($asClosure) { + $code = ' { '.preg_replace('/: static$/', ': \\'.$r->name, $code); + } else { + $code = ' implements \\'.$r->name.' { '.$code; + } + + $code = 'new class('.$initializer.') extends \\'.self::class + .$code.' { return $this->service->'.$callable[1].'(...\func_get_args()); } ' + .'}'; - return '(new class('.$initializer.') extends \\'.self::class.' { ' - .$signature.' { return $this->service->'.$method.'(...\func_get_args()); } ' - .'})->'.$method.'(...)'; + return $asClosure ? '('.$code.')->'.$method.'(...)' : $code; } } diff --git a/src/Symfony/Component/DependencyInjection/Attribute/AutowireCallable.php b/src/Symfony/Component/DependencyInjection/Attribute/AutowireCallable.php index 08fdc6e6904aa..c4a7632fa45d7 100644 --- a/src/Symfony/Component/DependencyInjection/Attribute/AutowireCallable.php +++ b/src/Symfony/Component/DependencyInjection/Attribute/AutowireCallable.php @@ -20,11 +20,14 @@ #[\Attribute(\Attribute::TARGET_PARAMETER)] class AutowireCallable extends Autowire { + /** + * @param bool|class-string $lazy Whether to use lazy-loading for this argument + */ public function __construct( string|array $callable = null, string $service = null, string $method = null, - bool $lazy = false, + bool|string $lazy = false, ) { if (!(null !== $callable xor null !== $service)) { throw new LogicException('#[AutowireCallable] attribute must declare exactly one of $callable or $service.'); diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index d61830ef742c7..b92ec95897b85 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -18,6 +18,7 @@ CHANGELOG * Add support for generating lazy closures * Add support for autowiring services as closures using `#[AutowireCallable]` or `#[AutowireServiceClosure]` * Add support for `#[Autowire(lazy: true|class-string)]` + * Make it possible to cast callables into single-method interfaces * Deprecate `#[MapDecorated]`, use `#[AutowireDecorated]` instead * Deprecate the `@required` annotation, use the `Symfony\Contracts\Service\Attribute\Required` attribute instead diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index dd5900bbefddf..a68d19ea30049 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -291,10 +291,10 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a $value = $this->processValue(new TypedReference($type ?: '?', $type ?: 'mixed', $invalidBehavior, $name, [$attribute, ...$target])); if ($attribute instanceof AutowireCallable) { - $value = (new Definition('Closure')) + $value = (new Definition($type = \is_string($attribute->lazy) ? $attribute->lazy : ($type ?: 'Closure'))) ->setFactory(['Closure', 'fromCallable']) - ->setArguments([$value + [1 => '__invoke']]) - ->setLazy($attribute->lazy); + ->setArguments([\is_array($value) ? $value + [1 => '__invoke'] : $value]) + ->setLazy($attribute->lazy || 'Closure' !== $type && 'callable' !== (string) $parameter->getType()); } elseif ($lazy = $attribute->lazy) { $definition = (new Definition($type)) ->setFactory('current') diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 57c0234725f7c..ca696673601ff 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -1051,9 +1051,9 @@ private function createService(Definition $definition, array &$inlineServices, b } $parameterBag = $this->getParameterBag(); - $class = ($parameterBag->resolveValue($definition->getClass()) ?: (['Closure', 'fromCallable'] === $definition->getFactory() ? 'Closure' : null)); + $class = $parameterBag->resolveValue($definition->getClass()) ?: (['Closure', 'fromCallable'] === $definition->getFactory() ? 'Closure' : null); - if ('Closure' === $class && $definition->isLazy() && ['Closure', 'fromCallable'] === $definition->getFactory()) { + if (['Closure', 'fromCallable'] === $definition->getFactory() && ('Closure' !== $class || $definition->isLazy())) { $callable = $parameterBag->unescapeValue($parameterBag->resolveValue($definition->getArgument(0))); if ($callable instanceof Reference || $callable instanceof Definition) { @@ -1065,19 +1065,18 @@ private function createService(Definition $definition, array &$inlineServices, b || $callable[0] instanceof Definition && !isset($inlineServices[spl_object_hash($callable[0])]) )) { $containerRef = $this->containerRef ??= \WeakReference::create($this); - $class = ($callable[0] instanceof Reference ? $this->findDefinition($callable[0]) : $callable[0])->getClass(); $initializer = static function () use ($containerRef, $callable, &$inlineServices) { return $containerRef->get()->doResolveServices($callable[0], $inlineServices); }; - $proxy = eval('return '.LazyClosure::getCode('$initializer', $this->getReflectionClass($class), $callable[1], $id).';'); + $proxy = eval('return '.LazyClosure::getCode('$initializer', $callable, $definition, $this, $id).';'); $this->shareService($definition, $proxy, $id, $inlineServices); return $proxy; } } - if (true === $tryProxy && $definition->isLazy() && 'Closure' !== $class + if (true === $tryProxy && $definition->isLazy() && ['Closure', 'fromCallable'] !== $definition->getFactory() && !$tryProxy = !($proxy = $this->proxyInstantiator ??= new LazyServiceInstantiator()) || $proxy instanceof RealServiceInstantiator ) { $containerRef = $this->containerRef ??= \WeakReference::create($this); diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 7f8054675a749..c8f1ebd2b1635 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -1162,7 +1162,8 @@ private function addNewInstance(Definition $definition, string $return = '', str if ('current' === $callable && [0] === array_keys($definition->getArguments()) && \is_array($value) && [0] === array_keys($value)) { return $return.$this->dumpValue($value[0]).$tail; } - if (['Closure', 'fromCallable'] === $callable && [0] === array_keys($definition->getArguments())) { + + if (['Closure', 'fromCallable'] === $callable) { $callable = $definition->getArgument(0); if ($callable instanceof ServiceClosureArgument) { return $return.$this->dumpValue($callable).$tail; @@ -1175,58 +1176,56 @@ private function addNewInstance(Definition $definition, string $return = '', str } } - if (\is_array($callable)) { - if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $callable[1])) { - 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') && ( - $callable[0] instanceof Reference - || ($callable[0] instanceof Definition && !$this->definitionVariables->contains($callable[0])) - )) { - $class = ($callable[0] instanceof Reference ? $this->container->findDefinition($callable[0]) : $callable[0])->getClass(); + if (\is_string($callable) && str_starts_with($callable, '@=')) { + return $return.sprintf('(($args = %s) ? (%s) : null)', + $this->dumpValue(new ServiceLocatorArgument($definition->getArguments())), + $this->getExpressionLanguage()->compile(substr($callable, 2), ['container' => 'container', 'args' => 'args']) + ).$tail; + } - if (str_contains($initializer = $this->dumpValue($callable[0]), '$container')) { - $this->addContainerRef = true; - $initializer = sprintf('function () use ($containerRef) { $container = $containerRef; return %s; }', $initializer); - } else { - $initializer = 'fn () => '.$initializer; - } + if (!\is_array($callable)) { + return $return.sprintf('%s(%s)', $this->dumpLiteralClass($this->dumpValue($callable)), $arguments ? implode(', ', $arguments) : '').$tail; + } - return $return.LazyClosure::getCode($initializer, $this->container->getReflectionClass($class), $callable[1], $id).$tail; - } + if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $callable[1])) { + throw new RuntimeException(sprintf('Cannot dump definition because of invalid factory method (%s).', $callable[1] ?: 'n/a')); + } - if ($callable[0] instanceof Reference - || ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0])) - ) { - return $return.sprintf('%s->%s(%s)', $this->dumpValue($callable[0]), $callable[1], $arguments ? implode(', ', $arguments) : '').$tail; + if (['...'] === $arguments && ($definition->isLazy() || 'Closure' !== ($definition->getClass() ?? 'Closure')) && ( + $callable[0] instanceof Reference + || ($callable[0] instanceof Definition && !$this->definitionVariables->contains($callable[0])) + )) { + if (str_contains($initializer = $this->dumpValue($callable[0]), '$container')) { + $this->addContainerRef = true; + $initializer = sprintf('function () use ($containerRef) { $container = $containerRef->get(); return %s; }', $initializer); + } else { + $initializer = 'fn () => '.$initializer; } - $class = $this->dumpValue($callable[0]); - // If the class is a string we can optimize away - if (str_starts_with($class, "'") && !str_contains($class, '$')) { - if ("''" === $class) { - throw new RuntimeException(sprintf('Cannot dump definition: "%s" service is defined to be created by a factory but is missing the service reference, did you forget to define the factory service id or class?', $id ? 'The "'.$id.'"' : 'inline')); - } + return $return.LazyClosure::getCode($initializer, $callable, $definition, $this->container, $id).$tail; + } - return $return.sprintf('%s::%s(%s)', $this->dumpLiteralClass($class), $callable[1], $arguments ? implode(', ', $arguments) : '').$tail; - } + if ($callable[0] instanceof Reference + || ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0])) + ) { + return $return.sprintf('%s->%s(%s)', $this->dumpValue($callable[0]), $callable[1], $arguments ? implode(', ', $arguments) : '').$tail; + } - if (str_starts_with($class, 'new ')) { - return $return.sprintf('(%s)->%s(%s)', $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail; + $class = $this->dumpValue($callable[0]); + // If the class is a string we can optimize away + if (str_starts_with($class, "'") && !str_contains($class, '$')) { + if ("''" === $class) { + throw new RuntimeException(sprintf('Cannot dump definition: "%s" service is defined to be created by a factory but is missing the service reference, did you forget to define the factory service id or class?', $id ? 'The "'.$id.'"' : 'inline')); } - return $return.sprintf("[%s, '%s'](%s)", $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail; + return $return.sprintf('%s::%s(%s)', $this->dumpLiteralClass($class), $callable[1], $arguments ? implode(', ', $arguments) : '').$tail; } - if (\is_string($callable) && str_starts_with($callable, '@=')) { - return $return.sprintf('(($args = %s) ? (%s) : null)', - $this->dumpValue(new ServiceLocatorArgument($definition->getArguments())), - $this->getExpressionLanguage()->compile(substr($callable, 2), ['container' => 'container', 'args' => 'args']) - ).$tail; + if (str_starts_with($class, 'new ')) { + return $return.sprintf('(%s)->%s(%s)', $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail; } - return $return.sprintf('%s(%s)', $this->dumpLiteralClass($this->dumpValue($callable)), $arguments ? implode(', ', $arguments) : '').$tail; + return $return.sprintf("[%s, '%s'](%s)", $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail; } if (null === $class = $definition->getClass()) { @@ -2344,7 +2343,7 @@ private function isProxyCandidate(Definition $definition, ?bool &$asGhostObject, { $asGhostObject = false; - if ('Closure' === ($definition->getClass() ?: (['Closure', 'fromCallable'] === $definition->getFactory() ? 'Closure' : null))) { + if (['Closure', 'fromCallable'] === $definition->getFactory()) { return null; } diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/FromCallableConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/FromCallableConfigurator.php new file mode 100644 index 0000000000000..7fe0d3da14c88 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/FromCallableConfigurator.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\DependencyInjection\Definition; + +/** + * @author Nicolas Grekas
+ */
+class FromCallableConfigurator extends AbstractServiceConfigurator
+{
+ use Traits\AbstractTrait;
+ use Traits\AutoconfigureTrait;
+ use Traits\AutowireTrait;
+ use Traits\BindTrait;
+ use Traits\DecorateTrait;
+ use Traits\DeprecateTrait;
+ use Traits\LazyTrait;
+ use Traits\PublicTrait;
+ use Traits\ShareTrait;
+ use Traits\TagTrait;
+
+ public const FACTORY = 'services';
+
+ private ServiceConfigurator $serviceConfigurator;
+
+ public function __construct(ServiceConfigurator $serviceConfigurator, Definition $definition)
+ {
+ $this->serviceConfigurator = $serviceConfigurator;
+
+ parent::__construct($serviceConfigurator->parent, $definition, $serviceConfigurator->id);
+ }
+
+ public function __destruct()
+ {
+ $this->serviceConfigurator->__destruct();
+ }
+}
diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ServiceConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ServiceConfigurator.php
index 49aff7ea947e7..2312f3b6e6e97 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ServiceConfigurator.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ServiceConfigurator.php
@@ -31,6 +31,7 @@ class ServiceConfigurator extends AbstractServiceConfigurator
use Traits\DeprecateTrait;
use Traits\FactoryTrait;
use Traits\FileTrait;
+ use Traits\FromCallableTrait;
use Traits\LazyTrait;
use Traits\ParentTrait;
use Traits\PropertyTrait;
diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FromCallableTrait.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FromCallableTrait.php
new file mode 100644
index 0000000000000..e3508ab89561d
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FromCallableTrait.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;
+
+use Symfony\Component\DependencyInjection\ChildDefinition;
+use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
+use Symfony\Component\DependencyInjection\Loader\Configurator\FromCallableConfigurator;
+use Symfony\Component\DependencyInjection\Loader\Configurator\ReferenceConfigurator;
+use Symfony\Component\ExpressionLanguage\Expression;
+
+trait FromCallableTrait
+{
+ final public function fromCallable(string|array|ReferenceConfigurator|Expression $callable): FromCallableConfigurator
+ {
+ if ($this->definition instanceof ChildDefinition) {
+ throw new InvalidArgumentException('The configuration key "parent" is unsupported when using "fromCallable()".');
+ }
+
+ foreach ([
+ 'synthetic' => 'isSynthetic',
+ 'factory' => 'getFactory',
+ 'file' => 'getFile',
+ 'arguments' => 'getArguments',
+ 'properties' => 'getProperties',
+ 'configurator' => 'getConfigurator',
+ 'calls' => 'getMethodCalls',
+ ] as $key => $method) {
+ if ($this->definition->$method()) {
+ throw new InvalidArgumentException(sprintf('The configuration key "%s" is unsupported when using "fromCallable()".', $key));
+ }
+ }
+
+ $this->definition->setFactory(['Closure', 'fromCallable']);
+
+ if (\is_string($callable) && 1 === substr_count($callable, ':')) {
+ $parts = explode(':', $callable);
+
+ throw new InvalidArgumentException(sprintf('Invalid callable "%s": the "service:method" notation is not available when using PHP-based DI configuration. Use "[service(\'%s\'), \'%s\']" instead.', $callable, $parts[0], $parts[1]));
+ }
+
+ if ($callable instanceof Expression) {
+ $callable = '@='.$callable;
+ }
+
+ $this->definition->setArguments([static::processValue($callable, true)]);
+
+ if ('Closure' !== ($this->definition->getClass() ?? 'Closure')) {
+ $this->definition->setLazy(true);
+ } else {
+ $this->definition->setClass('Closure');
+ }
+
+ return new FromCallableConfigurator($this, $this->definition);
+ }
+}
diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
index 6350b3af307d2..6ddecf5d54ab6 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
@@ -389,6 +389,52 @@ private function parseDefinition(\DOMElement $service, string $file, Definition
$definition->setDecoratedService($decorates, $renameId, $priority, $invalidBehavior);
}
+ if ($callable = $this->getChildren($service, 'from-callable')) {
+ if ($definition instanceof ChildDefinition) {
+ throw new InvalidArgumentException(sprintf('Attribute "parent" is unsupported when using "