8000 [DependencyInjection] add `Autowire` attribute · symfony/symfony@45d49dc · GitHub
[go: up one dir, main page]

Skip to content

Commit 45d49dc

Browse files
committed
[DependencyInjection] add Autowire attribute
1 parent 1df76df commit 45d49dc

File tree

8 files changed

+212
-0
lines changed
  • src/Symfony/Component
    • DependencyInjection
      • Attribute
  • Compiler
  • Tests
  • HttpKernel
  • 8 files changed

    +212
    -0
    lines changed
    Lines changed: 50 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,50 @@
    1+
    <?php
    2+
    3+
    /*
    4+
    * This file is part of the Symfony package.
    5+
    *
    6+
    * (c) Fabien Potencier <fabien@symfony.com>
    7+
    *
    8+
    * For the full copyright and license information, please view the LICENSE
    9+
    * file that was distributed with this source code.
    10+
    */
    11+
    12+
    namespace Symfony\Component\DependencyInjection\Attribute;
    13+
    14+
    use Symfony\Component\DependencyInjection\Exception\LogicException;
    15+
    use Symfony\Component\DependencyInjection\Reference;
    16+
    use Symfony\Component\ExpressionLanguage\Expression;
    17+
    18+
    /**
    19+
    * Attribute to tell a parameter how to be autowired.
    20+
    *
    21+
    * @author Kevin Bond <kevinbond@gmail.com>
    22+
    */
    23+
    #[\Attribute(\Attribute::TARGET_PARAMETER)]
    24+
    class Autowire
    25+
    {
    26+
    public readonly string|Expression|Reference $value;
    27+
    28+
    /**
    29+
    * Use only ONE of the following.
    30+
    *
    31+
    * @param string|null $service Service ID (ie "some.service")
    32+
    * @param string|null $expression Expression (ie 'service("some.service").someMethod()')
    33+
    * @param string|null $value Parameter value (ie "%kernel.project_dir%/some/path")
    34+
    */
    35+
    public function __construct(
    36+
    ?string $service = null,
    37+
    ?string $expression = null,
    38+
    ?string $value = null
    39+
    ) {
    40+
    if (!($service xor $expression xor null !== $value)) {
    41+
    throw new LogicException('#[Autowire] attribute must declare exactly one of $service, $expression, or $value.');
    42+
    }
    43+
    44+
    $this->value = match (true) {
    45+
    null !== $service => new Reference($service),
    46+
    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".'),
    47+
    null !== $value => $value,
    48+
    };
    49+
    }
    50+
    }

    src/Symfony/Component/DependencyInjection/CHANGELOG.md

    Lines changed: 1 addition & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -7,6 +7,7 @@ CHANGELOG
    77
    * Add `$exclude` to `TaggedIterator` and `TaggedLocator` attributes
    88
    * Add `$exclude` to `tagged_iterator` and `tagged_locator` configurator
    99
    * Add an `env` function to the expression language provider
    10+
    * Add an `Autowire` attribute to tell a parameter how to be autowired
    1011

    1112
    6.0
    1213
    ---

    src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php

    Lines changed: 15 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -14,14 +14,17 @@
    1414
    use Symfony\Component\Config\Resource\ClassExistenceResource;
    1515
    use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
    1616
    use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
    17+
    use Symfony\Component\DependencyInjection\Attribute\Autowire;
    1718
    use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
    1819
    use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
    1920
    use Symfony\Component\DependencyInjection\Attribute\Target;
    2021
    use Symfony\Component\DependencyInjection\ContainerBuilder;
    22+
    use Symfony\Component\DependencyInjection\ContainerInterface;
    2123
    use Symfony\Component\DependencyInjection\Definition;
    2224
    use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
    2325
    use Symfony\Component\DependencyInjection\Exception\RuntimeException;
    2426
    use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
    27+
    use Symfony\Component\DependencyInjection\Reference;
    2528
    use Symfony\Component\DependencyInjection\TypedReference;
    2629

    2730
    /**
    @@ -256,6 +259,18 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
    256259
    $arguments[$index] = new ServiceLocatorArgument(new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute, $attribute->defaultIndexMethod, true, $attribute->defaultPriorityMethod, (array) $attribute->exclude));
    257260
    break;
    258261
    }
    262+
    263+
    if (Autowire::class === $attribute->getName()) {
    264+
    $value = $attribute->newInstance()->value;
    265+
    266+
    if ($value instanceof Reference && $parameter->allowsNull()) {
    267+
    $value = new Reference($value, ContainerInterface::NULL_ON_INVALID_REFERENCE);
    268+
    }
    269+
    270+
    $arguments[$index] = $value;
    271+
    272+
    break;
    273+
    }
    259274
    }
    260275

    261276
    if ('' !== ($arguments[$index] ?? '')) {
    Lines changed: 38 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,38 @@
    1+
    <?php
    2+
    3+
    /*
    4+
    * This file is part of the Symfony package.
    5+
    *
    6+
    * (c) Fabien Potencier <fabien@symfony.com>
    7+
    *
    8+
    * For the full copyright and license information, please view the LICENSE
    9+
    * file that was distributed with this source code.
    10+
    */
    11+
    12+
    namespace Symfony\Component\DependencyInjection\Tests\Attribute;
    13+
    14+
    use PHPUnit\Framework\TestCase;
    15+
    use Symfony\Component\DependencyInjection\Attribute\Autowire;
    16+
    use Symfony\Component\DependencyInjection\Exception\LogicException;
    17+
    18+
    class AutowireTest extends TestCase
    19+
    {
    20+
    public function testCanOnlySetOneParameter()
    21+
    {
    22+
    $this->expectException(LogicException::class);
    23+
    24+
    new Autowire(service: 'id', expression: 'expr');
    25+
    }
    26+
    27+
    public function testMustSetOneParameter()
    28+
    {
    29+
    $this->expectException(LogicException::class);
    30+
    31+
    new Autowire();
    32+
    }
    33+
    34+
    public function testCanUseZeroForValue()
    35+
    {
    36+
    $this->assertSame('0', (new Autowire(value: '0'))->value);
    37+
    }
    38+
    }

    src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php

    Lines changed: 35 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -20,6 +20,7 @@
    2020
    use Symfony\Component\DependencyInjection\Compiler\DecoratorServicePass;
    2121
    use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass;
    2222
    use Symfony\Component\DependencyInjection\ContainerBuilder;
    23+
    use Symfony\Component\DependencyInjection\ContainerInterface;
    2324
    use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
    2425
    use Symfony\Component\DependencyInjection\Exception\RuntimeException;
    2526
    use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
    @@ -29,6 +30,7 @@
    2930
    use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\FooVariadic;
    3031
    use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget;
    3132
    use Symfony\Component\DependencyInjection\TypedReference;
    33+
    use Symfony\Component\ExpressionLanguage\Expression;
    3234
    use Symfony\Contracts\Service\Attribute\Required;
    3335

    3436
    require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php';
    @@ -1121,4 +1123,37 @@ public function testDecorationWithServiceAndAliasedInterface()
    11211123
    static::assertInstanceOf(DecoratedDecorator::class, $container->get(DecoratorInterface::class));
    11221124
    static::assertInstanceOf(DecoratedDecorator::class, $container->get(DecoratorImpl::class));
    11231125
    }
    1126+
    1127+
    public function testAutowireAttribute()
    1128+
    {
    1129+
    $container = new ContainerBuilder();
    1130+
    1131+
    $container->register(AutowireAttribute::class)
    1132+
    ->setAutowired(true)
    1133+
    ->setPublic(true)
    1134+
    ;
    1135+
    1136+
    $container->register('some.id', \stdClass::class);
    1137+
    $container->setParameter('some.parameter', 'foo');
    1138+
    1139+
    (new ResolveClassPass())->process($container);
    1140+
    (new AutowirePass())->process($container);
    1141+
    1142+
    $definition = $container->getDefinition(AutowireAttribute::class);
    1143+
    1144+
    $this->assertCount(4, $definition->getArguments());
    1145+
    $this->assertEquals(new Reference('some.id'), $definition->getArgument(0));
    1146+
    $this->assertEquals(new Expression("parameter('some.parameter')"), $definition->getArgument(1));
    1147+
    $this->assertSame('%some.parameter%/bar', $definition->getArgument(2));
    1148+
    $this->assertEquals(new Reference('invalid.id', ContainerInterface::NULL_ON_INVALID_REFERENCE), $definition->getArgument(3));
    1149+
    1150+
    $container->compile();
    1151+
    1152+
    $service = $container->get(AutowireAttribute::class);
    1153+
    1154+
    $this->assertInstanceOf(\stdClass::class, $service->service);
    1155+
    $this->assertSame('foo', $service->expression);
    1156+
    $this->assertSame('foo/bar', $service->value);
    1157+
    $this->assertNull($service->invalid);
    1158+
    }
    11241159
    }

    src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php

    Lines changed: 16 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -2,6 +2,7 @@
    22

    33
    namespace Symfony\Component\DependencyInjection\Tests\Compiler;
    44

    5+
    use Symfony\Component\DependencyInjection\Attribute\Autowire;
    56
    use Symfony\Contracts\Service\Attribute\Required;
    67

    78
    class AutowireSetter
    @@ -26,3 +27,18 @@ class AutowireProperty
    2627
    #[Required]
    2728
    public Foo $foo;
    2829
    }
    30+
    31+
    class AutowireAttribute
    32+
    {
    33+
    public function __construct(
    34+
    #[Autowire(service: 'some.id')]
    35+
    public \stdClass $service,
    36+
    #[Autowire(expression: "parameter('some.parameter')")]
    37+
    public string $expression,
    38+
    #[Autowire(value: '%some.parameter%/bar')]
    39+
    public string $value,
    40+
    #[Autowire(service: 'invalid.id')]
    41+
    public ?\stdClass $invalid = null,
    42+
    ) {
    43+
    }
    44+
    }

    src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php

    Lines changed: 14 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -12,6 +12,7 @@
    1212
    namespace Symfony\Component\HttpKernel\DependencyInjection;
    1313

    1414
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    15+
    use Symfony\Component\DependencyInjection\Attribute\Autowire;
    1516
    use Symfony\Component\DependencyInjection\Attribute\Target;
    1617
    use Symfony\Component\DependencyInjection\ChildDefinition;
    1718
    use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
    @@ -120,6 +121,19 @@ public function process(ContainerBuilder $container)
    120121
    $args = [];
    121122
    foreach ($parameters as $p) {
    122123
    /** @var \ReflectionParameter $p */
    124+
    if ($attr = $p->getAttributes(Autowire::class)[0]?->newInstance()) {
    125+
    $value = $attr->value;
    126+
    127+
    // TODO: refactor, support parameters/expressions
    128+
    if ($value instanceof Reference) {
    129+
    $value = new Reference($value, $p->allowsNull() ? ContainerInterface::NULL_ON_INVALID_REFERENCE : ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE);
    130+
    }
    131+
    132+
    $args[$p->name] = $value;
    133+
    134+
    continue;
    135+
    }
    136+
    123137
    $type = ltrim($target = (string) ProxyHelper::getTypeHint($r, $p), '\\');
    124138
    $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
    125139

    src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php

    Lines changed: 43 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -13,6 +13,7 @@
    1313

    1414
    use PHPUnit\Framework\TestCase;
    1515
    use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
    16+
    use Symfony\Component\DependencyInjection\Attribute\Autowire;
    1617
    use Symfony\Component\DependencyInjection\Attribute\Target;
    1718
    use Symfony\Component\DependencyInjection\ChildDefinition;
    1819
    use Symfony\Component\DependencyInjection\ContainerAwareInterface;
    @@ -434,6 +435,33 @@ public function testBindWithTarget()
    434435
    $expected = ['apiKey' => new ServiceClosureArgument(new Reference('the_api_key'))];
    435436
    $this->assertEquals($expected, $locator->getArgument(0));
    436437
    }
    438+
    439+
    public function testAutowireAttribute()
    440+
    {
    441+
    $container = new ContainerBuilder();
    442+
    $resolver = $container->register('argument_resolver.service')->addArgument([]);
    443+
    444+
    $container->register('some.id', \stdClass::class);
    445+
    $container->setParameter('some.parameter', 'foo');
    446+
    447+
    $container->register('foo', WithAutowireAttribute::class)
    448+
    ->addTag('controller.service_arguments');
    449+
    450+
    (new RegisterControllerArgumentLocatorsPass())->process($container);
    451+
    452+
    $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
    453+
    $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
    454+
    455+
    dd($locator->getArgument(0));
    456+
    457+
    $expected = [
    458+
    'service1' => new ServiceClosureArgument(new Reference('some.id', ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE)),
    459+
    'value' => '???',
    460+
    'expression' => '???',
    461+
    'service2' => new ServiceClosureArgument(new Reference('invalid.id', ContainerInterface::NULL_ON_INVALID_REFERENCE)),
    462+
    ];
    463+
    $this->assertEquals($expected, $locator->getArgument(0));
    464+
    }
    437465
    }
    438466

    439467
    class RegisterTestController
    @@ -511,3 +539,18 @@ public function fooAction(
    511539
    ) {
    512540
    }
    513541
    }
    542+
    543+
    class WithAutowireAttribute
    544+
    {
    545+
    public function fooAction(
    546+
    #[Autowire(service: 'some.id')]
    547+
    \stdClass $service1,
    548+
    #[Autowire(value: '%some.parameter%/bar')]
    549+
    string $value,
    550+
    #[Autowire(expression: "parameter('some.parameter')")]
    551+
    string $expression,
    552+
    #[Autowire(service: 'invalid.id')]
    553+
    \stdClass $service2 = null,
    554+
    ) {
    555+
    }
    556+
    }

    0 commit comments

    Comments
     (0)
    0