Skip to content

Commit

Permalink
feature #45657 [DependencyInjection] add Autowire parameter attribu…
Browse files Browse the repository at this point in the history
…te (kbond)

This PR was merged into the 6.1 branch.

Discussion
----------

[DependencyInjection] add `Autowire` parameter attribute

| Q             | A
| ------------- | ---
| Branch?       | 6.1
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | n/a
| License       | MIT
| Doc PR        | todo

Replaces #45573 & #44780 with a single new `Autowire` attribute:

```php
class MyService
{
    public function __construct(
        #[Autowire(service: 'some_service')]
        private $service1,

        #[Autowire(expression: 'service("App\\Mail\\MailerConfiguration").getMailerMethod()')
        private $service2,

        #[Autowire(value: '%env(json:file:resolve:AUTH_FILE)%')]
        private $parameter1,

        #[Autowire(value: '%kernel.project_dir%/config/dir')]
        private $parameter2,
    ) {}
}
```

Works with controller arguments as well:

```php
class MyController
{
    public function someAction(
        #[Autowire(service: 'some_service')]
        $service1,

        #[Autowire(expression: 'service("App\\Mail\\MailerConfiguration").getMailerMethod()')
        $service2,

        #[Autowire(value: '%env(json:file:resolve:AUTH_FILE)%')]
        $parameter1,

        #[Autowire(value: '%kernel.project_dir%/config/dir')]
        $parameter2,
    ): Response {}
}
```

Commits
-------

d43fe42 [DependencyInjection] add `Autowire` parameter attribute
  • Loading branch information
fabpot committed Mar 18, 2022
2 parents 69f02aa + d43fe42 commit 7248e16
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 1 deletion.
50 changes: 50 additions & 0 deletions src/Symfony/Component/DependencyInjection/Attribute/Autowire.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
use Symfony\Component\ExpressionLanguage\Expression;

/**
* Attribute to tell a parameter how to be autowired.
*
* @author Kevin Bond <kevinbond@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class Autowire
{
public readonly string|Expression|Reference $value;

/**
* Use only ONE of the following.
*
* @param string|null $service Service ID (ie "some.service")
* @param string|null $expression Expression (ie 'service("some.service").someMethod()')
* @param string|null $value Parameter value (ie "%kernel.project_dir%/some/path")
*/
public function __construct(
?string $service = null,
?string $expression = null,
?string $value = null
) {
if (!($service xor $expression xor null !== $value)) {
throw new LogicException('#[Autowire] attribute must declare exactly one of $service, $expression, or $value.');
}

$this->value = match (true) {
null !== $service => new Reference($service),
null !== $expression => class_exists(Expression::class) ? new Expression($expression) : throw new LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".'),
null !== $value => $value,
};
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ CHANGELOG
* Add `$exclude` to `TaggedIterator` and `TaggedLocator` attributes
* Add `$exclude` to `tagged_iterator` and `tagged_locator` configurator
* Add an `env` function to the expression language provider
* Add an `Autowire` attribute to tell a parameter how to be autowired

6.0
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@
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\TaggedIterator;
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\TypedReference;

/**
Expand Down Expand Up @@ -256,6 +259,18 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
$arguments[$index] = new ServiceLocatorArgument(new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute, $attribute->defaultIndexMethod, true, $attribute->defaultPriorityMethod, (array) $attribute->exclude));
break;
}

if (Autowire::class === $attribute->getName()) {
$value = $attribute->newInstance()->value;

if ($value instanceof Reference && $parameter->allowsNull()) {
$value = new Reference($value, ContainerInterface::NULL_ON_INVALID_REFERENCE);
}

$arguments[$index] = $value;

break;
}
}

if ('' !== ($arguments[$index] ?? '')) {
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 <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\DependencyInjection\Tests\Attribute;

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Exception\LogicException;

class AutowireTest extends TestCase
{
public function testCanOnlySetOneParameter()
{
$this->expectException(LogicException::class);

new Autowire(service: 'id', expression: 'expr');
}

public function testMustSetOneParameter()
{
$this->expectException(LogicException::class);

new Autowire();
}

public function testCanUseZeroForValue()
{
$this->assertSame('0', (new Autowire(value: '0'))->value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Symfony\Component\DependencyInjection\Compiler\DecoratorServicePass;
use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
Expand All @@ -29,6 +30,7 @@
use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\FooVariadic;
use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget;
use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Contracts\Service\Attribute\Required;

require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php';
Expand Down Expand Up @@ -1121,4 +1123,37 @@ public function testDecorationWithServiceAndAliasedInterface()
static::assertInstanceOf(DecoratedDecorator::class, $container->get(DecoratorInterface::class));
static::assertInstanceOf(DecoratedDecorator::class, $container->get(DecoratorImpl::class));
}

public function testAutowireAttribute()
{
$container = new ContainerBuilder();

$container->register(AutowireAttribute::class)
->setAutowired(true)
->setPublic(true)
;

$container->register('some.id', \stdClass::class);
$container->setParameter('some.parameter', 'foo');

(new ResolveClassPass())->process($container);
(new AutowirePass())->process($container);

$definition = $container->getDefinition(AutowireAttribute::class);

$this->assertCount(4, $definition->getArguments());
$this->assertEquals(new Reference('some.id'), $definition->getArgument(0));
$this->assertEquals(new Expression("parameter('some.parameter')"), $definition->getArgument(1));
$this->assertSame('%some.parameter%/bar', $definition->getArgument(2));
$this->assertEquals(new Reference('invalid.id', ContainerInterface::NULL_ON_INVALID_REFERENCE), $definition->getArgument(3));

$container->compile();

$service = $container->get(AutowireAttribute::class);

$this->assertInstanceOf(\stdClass::class, $service->service);
$this->assertSame('foo', $service->expression);
$this->assertSame('foo/bar', $service->value);
$this->assertNull($service->invalid);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Symfony\Component\DependencyInjection\Tests\Compiler;

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\Service\Attribute\Required;

class AutowireSetter
Expand All @@ -26,3 +27,18 @@ class AutowireProperty
#[Required]
public Foo $foo;
}

class AutowireAttribute
{
public function __construct(
#[Autowire(service: 'some.id')]
public \stdClass $service,
#[Autowire(expression: "parameter('some.parameter')")]
public string $expression,
#[Autowire(value: '%some.parameter%/bar')]
public string $value,
#[Autowire(service: 'invalid.id')]
public ?\stdClass $invalid = null,
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\HttpKernel\DependencyInjection;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
Expand Down Expand Up @@ -49,6 +50,8 @@ public function process(ContainerBuilder $container)
}
}

$emptyAutowireAttributes = class_exists(Autowire::class) ? null : [];

foreach ($container->findTaggedServiceIds('controller.service_arguments', true) as $id => $tags) {
$def = $container->getDefinition($id);
$def->setPublic(true);
Expand Down Expand Up @@ -122,6 +125,7 @@ public function process(ContainerBuilder $container)
/** @var \ReflectionParameter $p */
$type = ltrim($target = (string) ProxyHelper::getTypeHint($r, $p), '\\');
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
$autowireAttributes = $autowire ? $emptyAutowireAttributes : [];

if (isset($arguments[$r->name][$p->name])) {
$target = $arguments[$r->name][$p->name];
Expand All @@ -148,7 +152,7 @@ public function process(ContainerBuilder $container)
}

continue;
} elseif (!$type || !$autowire || '\\' !== $target[0]) {
} elseif (!$autowire || (!($autowireAttributes ??= $p->getAttributes(Autowire::class)) && (!$type || '\\' !== $target[0]))) {
continue;
} elseif (is_subclass_of($type, \UnitEnum::class)) {
// do not attempt to register enum typed arguments if not already present in bindings
Expand All @@ -161,6 +165,21 @@ public function process(ContainerBuilder $container)
continue;
}

if ($autowireAttributes) {
$value = $autowireAttributes[0]->newInstance()->value;

if ($value instanceof Reference) {
$args[$p->name] = $type ? new TypedReference($value, $type, $invalidBehavior, $p->name) : new Reference($value, $invalidBehavior);
} else {
$args[$p->name] = new Reference('.value.'.$container->hash($value));
$container->register((string) $args[$p->name], 'mixed')
->setFactory('current')
->addArgument([$value]);
}

continue;
}

if ($type && !$p->isOptional() && !$p->allowsNull() && !class_exists($type) && !interface_exists($type, false)) {
$message = sprintf('Cannot determine controller argument for "%s::%s()": the $%s argument is type-hinted with the non-existent class or interface: "%s".', $class, $r->name, $p->name, $type);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
Expand Down Expand Up @@ -441,6 +442,36 @@ public function testBindWithTarget()
];
$this->assertEquals($expected, $locator->getArgument(0));
}

public function testAutowireAttribute()
{
if (!class_exists(Autowire::class)) {
$this->markTestSkipped('#[Autowire] attribute not available.');
}

$container = new ContainerBuilder();
$resolver = $container->register('argument_resolver.service', 'stdClass')->addArgument([]);

$container->register('some.id', \stdClass::class);
$container->setParameter('some.parameter', 'foo');

$container->register('foo', WithAutowireAttribute::class)
->addTag('controller.service_arguments');

(new RegisterControllerArgumentLocatorsPass())->process($container);

$locatorId = (string) $resolver->getArgument(0);
$container->getDefinition($locatorId)->setPublic(true);

$container->compile();

$locator = $container->get($locatorId)->get('foo::fooAction');

$this->assertInstanceOf(\stdClass::class, $locator->get('service1'));
$this->assertSame('foo/bar', $locator->get('value'));
$this->assertSame('foo', $locator->get('expression'));
$this->assertFalse($locator->has('service2'));
}
}

class RegisterTestController
Expand Down Expand Up @@ -521,3 +552,18 @@ public function fooAction(
) {
}
}

class WithAutowireAttribute
{
public function fooAction(
#[Autowire(service: 'some.id')]
\stdClass $service1,
#[Autowire(value: '%some.parameter%/bar')]
string $value,
#[Autowire(expression: "parameter('some.parameter')")]
string $expression,
#[Autowire(service: 'invalid.id')]
\stdClass $service2 = null,
) {
}
}

0 comments on commit 7248e16

Please sign in to comment.