Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit d43fe42

Browse filesBrowse files
kbondnicolas-grekas
authored andcommitted
[DependencyInjection] add Autowire parameter attribute
1 parent 1df76df commit d43fe42
Copy full SHA for d43fe42

File tree

8 files changed

+221
-1
lines changed
Filter options

8 files changed

+221
-1
lines changed
+50Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/CHANGELOG.md
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php
+15Lines 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] ?? '')) {
+38Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php
+35Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php
+16Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php
+20-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php
+46Lines 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;
@@ -434,6 +435,36 @@ 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+
if (!class_exists(Autowire::class)) {
442+
$this->markTestSkipped('#[Autowire] attribute not available.');
443+
}
444+
445+
$container = new ContainerBuilder();
446+
$resolver = $container->register('argument_resolver.service', 'stdClass')->addArgument([]);
447+
448+
$container->register('some.id', \stdClass::class);
449+
$container->setParameter('some.parameter', 'foo');
450+
451+
$container->register('foo', WithAutowireAttribute::class)
452+
->addTag('controller.service_arguments');
453+
454+
(new RegisterControllerArgumentLocatorsPass())->process($container);
455+
456+
$locatorId = (string) $resolver->getArgument(0);
457+
$container->getDefinition($locatorId)->setPublic(true);
458+
459+
$container->compile();
460+
461+
$locator = $container->get($locatorId)->get('foo::fooAction');
462+
463+
$this->assertInstanceOf(\stdClass::class, $locator->get('service1'));
464+
$this->assertSame('foo/bar', $locator->get('value'));
465+
$this->assertSame('foo', $locator->get('expression'));
466+
$this->assertFalse($locator->has('service2'));
467+
}
437468
}
438469

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

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.