Skip to content

Commit 7248e16

Browse files
committed
feature symfony#45657 [DependencyInjection] add Autowire parameter attribute (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 symfony#45573 & symfony#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
2 parents 69f02aa + d43fe42 commit 7248e16

File tree

8 files changed

+221
-1
lines changed

8 files changed

+221
-1
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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: 20 additions & 1 deletion
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;
@@ -49,6 +50,8 @@ public function process(ContainerBuilder $container)
4950
}
5051
}
5152

53+
$emptyAutowireAttributes = class_exists(Autowire::class) ? null : [];
54+
5255
foreach ($container->findTaggedServiceIds('controller.service_arguments', true) as $id => $tags) {
5356
$def = $container->getDefinition($id);
5457
$def->setPublic(true);
@@ -122,6 +125,7 @@ public function process(ContainerBuilder $container)
122125
/** @var \ReflectionParameter $p */
123126
$type = ltrim($target = (string) ProxyHelper::getTypeHint($r, $p), '\\');
124127
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
128+
$autowireAttributes = $autowire ? $emptyAutowireAttributes : [];
125129

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

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

168+
if ($autowireAttributes) {
169+
$value = $autowireAttributes[0]->newInstance()->value;
170+
171+
if ($value instanceof Reference) {
172+
$args[$p->name] = $type ? new TypedReference($value, $type, $invalidBehavior, $p->name) : new Reference($value, $invalidBehavior);
173+
} else {
174+
$args[$p->name] = new Reference('.value.'.$container->hash($value));
175+
$container->register((string) $args[$p->name], 'mixed')
176+
->setFactory('current')
177+
->addArgument([$value]);
178+
}
179+
180+
continue;
181+
}
182+
164183
if ($type && !$p->isOptional() && !$p->allowsNull() && !class_exists($type) && !interface_exists($type, false)) {
165184
$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);
166185

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

Lines changed: 46 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;
@@ -441,6 +442,36 @@ public function testBindWithTarget()
441442
];
442443
$this->assertEquals($expected, $locator->getArgument(0));
443444
}
445+
446+
public function testAutowireAttribute()
447+
{
448+
if (!class_exists(Autowire::class)) {
449+
$this->markTestSkipped('#[Autowire] attribute not available.');
450+
}
451+
452+
$container = new ContainerBuilder();
453+
$resolver = $container->register('argument_resolver.service', 'stdClass')->addArgument([]);
454+
455+
$container->register('some.id', \stdClass::class);
456+
$container->setParameter('some.parameter', 'foo');
457+
458+
$container->register('foo', WithAutowireAttribute::class)
459+
->addTag('controller.service_arguments');
460+
461+
(new RegisterControllerArgumentLocatorsPass())->process($container);
462+
463+
$locatorId = (string) $resolver->getArgument(0);
464+
$container->getDefinition($locatorId)->setPublic(true);
465+
466+
$container->compile();
467+
468+
$locator = $container->get($locatorId)->get('foo::fooAction');
469+
470+
$this->assertInstanceOf(\stdClass::class, $locator->get('service1'));
471+
$this->assertSame('foo/bar', $locator->get('value'));
472+
$this->assertSame('foo', $locator->get('expression'));
473+
$this->assertFalse($locator->has('service2'));
474+
}
444475
}
445476

446477
class RegisterTestController
@@ -521,3 +552,18 @@ public function fooAction(
521552
) {
522553
}
523554
}
555+
556+
class WithAutowireAttribute
557+
{
558+
public function fooAction(
559+
#[Autowire(service: 'some.id')]
560+
\stdClass $service1,
561+
#[Autowire(value: '%some.parameter%/bar')]
562+
string $value,
563+
#[Autowire(expression: "parameter('some.parameter')")]
564+
string $expression,
565+
#[Autowire(service: 'invalid.id')]
566+
\stdClass $service2 = null,
567+
) {
568+
}
569+
}

0 commit comments

Comments
 (0)