Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[DependencyInjection] Add support for autowiring services as closures using attributes #49628

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions src/Symfony/Component/DependencyInjection/Attribute/Autowire.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\DependencyInjection\Attribute;

use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ExpressionLanguage\Expression;
Expand All @@ -23,19 +24,19 @@
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class Autowire
{
public readonly string|array|Expression|Reference $value;
public readonly string|array|Expression|Reference|ArgumentInterface $value;

/**
* Use only ONE of the following.
*
* @param string|array|null $value Parameter value (ie "%kernel.project_dir%/some/path")
* @param string|null $service Service ID (ie "some.service")
* @param string|null $expression Expression (ie 'service("some.service").someMethod()')
* @param string|null $env Environment variable name (ie 'SOME_ENV_VARIABLE')
* @param string|null $param Parameter name (ie 'some.parameter.name')
* @param string|array|ArgumentInterface|null $value Value to inject (ie "%kernel.project_dir%/some/path")
* @param string|null $service Service ID (ie "some.service")
* @param string|null $expression Expression (ie 'service("some.service").someMethod()')
* @param string|null $env Environment variable name (ie 'SOME_ENV_VARIABLE')
* @param string|null $param Parameter name (ie 'some.parameter.name')
*/
public function __construct(
string|array $value = null,
string|array|ArgumentInterface $value = null,
string $service = null,
string $expression = null,
string $env = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\DependencyInjection\Attribute;

use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Reference;

/**
* Attribute to tell which callable to give to an argument of type Closure.
*/
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class AutowireCallable extends Autowire
{
public function __construct(
string|array $callable = null,
string $service = null,
string $method = null,
public bool $lazy = false,
) {
if (!(null !== $callable xor null !== $service)) {
throw new LogicException('#[AutowireCallable] attribute must declare exactly one of $callable or $service.');
}
if (!(null !== $callable xor null !== $method)) {
throw new LogicException('#[AutowireCallable] attribute must declare one of $callable or $method.');
}

parent::__construct($callable ?? [new Reference($service), $method ?? '__invoke']);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\DependencyInjection\Attribute;

use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Reference;

/**
* Attribute to wrap a service in a closure that returns it.
*/
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class AutowireServiceClosure extends Autowire
{
public function __construct(string $service)
{
parent::__construct(new ServiceClosureArgument(new Reference($service)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@

namespace Symfony\Component\DependencyInjection\Attribute;

use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;

#[\Attribute(\Attribute::TARGET_PARAMETER)]
class TaggedIterator
class TaggedIterator extends Autowire
{
public function __construct(
public string $tag,
Expand All @@ -22,5 +24,6 @@ public function __construct(
public string|array $exclude = [],
public bool $excludeSelf = true,
) {
parent::__construct(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, false, $defaultPriorityMethod, (array) $exclude, $excludeSelf));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@

namespace Symfony\Component\DependencyInjection\Attribute;

use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;

#[\Attribute(\Attribute::TARGET_PARAMETER)]
class TaggedLocator
class TaggedLocator extends Autowire
{
public function __construct(
public string $tag,
Expand All @@ -22,5 +25,6 @@ public function __construct(
public string|array $exclude = [],
public bool $excludeSelf = true,
) {
parent::__construct(new ServiceLocatorArgument(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, true, $defaultPriorityMethod, (array) $exclude, $excludeSelf)));
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ CHANGELOG
* Allow to trim XML service parameters value by using `trim="true"` attribute
* Allow extending the `Autowire` attribute
* Add `#[Exclude]` to skip autoregistering a class
* Add support for autowiring services as closures using `#[AutowireCallable]` or `#[AutowireServiceClosure]`

6.2
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,9 @@
namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\Config\Resource\ClassExistenceResource;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;
use Symfony\Component\DependencyInjection\Attribute\MapDecorated;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
Expand Down Expand Up @@ -86,14 +83,6 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
return $this->processValue($this->container->getParameterBag()->resolveValue($value->value));
}

if ($value instanceof TaggedIterator) {
return new TaggedIteratorArgument($value->tag, $value->indexAttribute, $value->defaultIndexMethod, false, $value->defaultPriorityMethod, (array) $value->exclude, $value->excludeSelf);
}

if ($value instanceof TaggedLocator) {
return new ServiceLocatorArgument(new TaggedIteratorArgument($value->tag, $value->indexAttribute, $value->defaultIndexMethod, true, $value->defaultPriorityMethod, (array) $value->exclude, $value->excludeSelf));
}

if ($value instanceof MapDecorated) {
$definition = $this->container->getDefinition($this->currentId);

Expand Down Expand Up @@ -191,8 +180,6 @@ private function processAttribute(object $attribute, bool $isOptional = false):
return new Reference($attribute->value, ContainerInterface::NULL_ON_INVALID_REFERENCE);
}
// no break
case $attribute instanceof TaggedIterator:
case $attribute instanceof TaggedLocator:
case $attribute instanceof MapDecorated:
return $this->processValue($attribute);
}
Expand Down Expand Up @@ -291,17 +278,32 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
continue;
}

if ($checkAttributes) {
foreach ([TaggedIterator::class, TaggedLocator::class, Autowire::class, MapDecorated::class] as $attributeClass) {
foreach ($parameter->getAttributes($attributeClass, Autowire::class === $attributeClass ? \ReflectionAttribute::IS_INSTANCEOF : 0) as $attribute) {
$arguments[$index] = $this->processAttribute($attribute->newInstance(), $parameter->allowsNull());
$type = ProxyHelper::exportType($parameter, true);

continue 3;
if ($checkAttributes) {
foreach ($parameter->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
$attribute = $attribute->newInstance();
$value = $this->processAttribute($attribute, $parameter->allowsNull());

if ($attribute instanceof AutowireCallable || 'Closure' === $type && \is_array($value)) {
$value = (new Definition('Closure'))
->setFactory(['Closure', 'fromCallable'])
->setArguments([$value + [1 => '__invoke']])
->setLazy($attribute instanceof AutowireCallable && $attribute->lazy);
}
$arguments[$index] = $value;

continue 2;
}

foreach ($parameter->getAttributes(MapDecorated::class) as $attribute) {
$arguments[$index] = $this->processAttribute($attribute->newInstance(), $parameter->allowsNull());

continue 2;
}
}

if (!$type = ProxyHelper::exportType($parameter, true)) {
if (!$type) {
if (isset($arguments[$index])) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ public static function getSubscribedServices(): array
'autowired' => new ServiceClosureArgument(new Reference('service.id')),
'autowired.nullable' => new ServiceClosureArgument(new Reference('service.id', ContainerInterface::NULL_ON_INVALID_REFERENCE)),
'autowired.parameter' => new ServiceClosureArgument('foobar'),
'map.decorated' => new ServiceClosureArgument(new Reference('.service_locator.LnJLtj2.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)),
'map.decorated' => new ServiceClosureArgument(new Reference('.service_locator.EeZIdVM.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)),
'target' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'someTarget', [new Target('someTarget')])),
];
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocator as ArgumentServiceLocator;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;
use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\Container;
Expand Down Expand Up @@ -1651,6 +1654,38 @@ public function testClosure()

$this->assertStringEqualsFile(self::$fixturesPath.'/php/closure.php', $dumper->dump());
}

public function testAutowireClosure()
{
$container = new ContainerBuilder();
$container->register('foo', Foo::class)
->setPublic('true');
$container->register('baz', \Closure::class)
->setFactory(['Closure', 'fromCallable'])
->setArguments(['var_dump'])
->setPublic('true');
$container->register('bar', LazyConsumer::class)
->setPublic('true')
->setAutowired(true);
$container->compile();
$dumper = new PhpDumper($container);

$this->assertStringEqualsFile(self::$fixturesPath.'/php/autowire_closure.php', $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_Autowire_Closure']));

require self::$fixturesPath.'/php/autowire_closure.php';

$container = new \Symfony_DI_PhpDumper_Test_Autowire_Closure();

$this->assertInstanceOf(Foo::class, $container->get('foo'));
$this->assertInstanceOf(LazyConsumer::class, $bar = $container->get('bar'));
$this->assertInstanceOf(\Closure::class, $bar->foo);
$this->assertInstanceOf(\Closure::class, $bar->baz);
$this->assertInstanceOf(\Closure::class, $bar->buz);
$this->assertSame($container->get('foo'), ($bar->foo)());
$this->assertSame($container->get('baz'), $bar->baz);
$this->assertInstanceOf(Foo::class, $fooClone = ($bar->buz)());
$this->assertNotSame($container->get('foo'), $fooClone);
}
}

class Rot13EnvVarProcessor implements EnvVarProcessorInterface
Expand All @@ -1676,3 +1711,16 @@ public function __construct(\stdClass $a, \stdClass $b)
$this->bClone = clone $b;
}
}

class LazyConsumer
{
public function __construct(
#[AutowireServiceClosure('foo')]
public \Closure $foo,
#[Autowire(service: 'baz')]
public \Closure $baz,
#[AutowireCallable(service: 'foo', method: 'cloneFoo')]
public \Closure $buz,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;

/**
* @internal This class has been auto-generated by the Symfony Dependency Injection Component.
*/
class Symfony_DI_PhpDumper_Test_Autowire_Closure extends Container
{
protected $parameters = [];
protected readonly \WeakReference $ref;

public function __construct()
{
$this->ref = \WeakReference::create($this);
$this->services = $this->privates = [];
$this->methodMap = [
'bar' => 'getBarService',
'baz' => 'getBazService',
'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;
}

/**
* Gets the public 'bar' shared autowired service.
*
* @return \Symfony\Component\DependencyInjection\Tests\Dumper\LazyConsumer
*/
protected static function getBarService($container)
{
$containerRef = $container->ref;

return $container->services['bar'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\LazyConsumer(#[\Closure(name: 'foo', class: 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo')] function () use ($containerRef) {
$container = $containerRef->get();

return ($container->services['foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo());
}, ($container->services['baz'] ?? self::getBazService($container)), ($container->services['foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo())->cloneFoo(...));
}

/**
* Gets the public 'baz' shared service.
*
* @return \Closure
*/
protected static function getBazService($container)
{
return $container->services['baz'] = \var_dump(...);
}

/**
* 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();
}
}