Skip to content

Commit b9360ad

Browse files
DaDeathernicolas-grekas
authored andcommitted
[DependencyInjection] Add #[AutowireInline] attribute to allow service definition at the class level
1 parent 4a57a11 commit b9360ad

12 files changed

+739
-5
lines changed

Attribute/AutowireCallable.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
* Attribute to tell which callable to give to an argument of type Closure.
2020
*/
2121
#[\Attribute(\Attribute::TARGET_PARAMETER)]
22-
class AutowireCallable extends Autowire
22+
class AutowireCallable extends AutowireInline
2323
{
2424
/**
2525
* @param string|array|null $callable The callable to autowire
@@ -40,7 +40,7 @@ public function __construct(
4040
throw new LogicException('#[AutowireCallable] attribute cannot have a $method without a $service.');
4141
}
4242

43-
parent::__construct($callable ?? [new Reference($service), $method ?? '__invoke'], lazy: $lazy);
43+
Autowire::__construct($callable ?? [new Reference($service), $method ?? '__invoke'], lazy: $lazy);
4444
}
4545

4646
public function buildDefinition(mixed $value, ?string $type, \ReflectionParameter $parameter): Definition

Attribute/AutowireInline.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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\Definition;
15+
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
16+
17+
/**
18+
* Allows inline service definition for a constructor argument.
19+
* Using this attribute on a class autowires it as a new instance
20+
* which is not shared between different services.
21+
*
22+
* @author Ismail Özgün Turan <[email protected]>
23+
*/
24+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
25+
class AutowireInline extends Autowire
26+
{
27+
public function __construct(string|array $class, array $arguments = [], array $calls = [], array $properties = [], ?string $parent = null, bool|string $lazy = false)
28+
{
29+
parent::__construct([
30+
\is_array($class) ? 'factory' : 'class' => $class,
31+
'arguments' => $arguments,
32+
'calls' => $calls,
33+
'properties' => $properties,
34+
'parent' => $parent,
35+
], lazy: $lazy);
36+
}
37+
38+
public function buildDefinition(mixed $value, ?string $type, \ReflectionParameter $parameter): Definition
39+
{
40+
static $parseDefinition;
41+
static $yamlLoader;
42+
43+
$parseDefinition ??= new \ReflectionMethod(YamlFileLoader::class, 'parseDefinition');
44+
$yamlLoader ??= $parseDefinition->getDeclaringClass()->newInstanceWithoutConstructor();
45+
46+
if (isset($value['factory'])) {
47+
$value['class'] = $type;
48+
$value['factory'][0] ??= $type;
49+
$value['factory'][1] ??= '__invoke';
50+
}
51+
$class = $parameter->getDeclaringClass();
52+
53+
return $parseDefinition->invoke($yamlLoader, $class->name, $value, $class->getFileName(), ['autowire' => true], true);
54+
}
55+
}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ CHANGELOG
1313
* Add argument `$prepend` to `FileLoader::construct()` to prepend loaded configuration instead of appending it
1414
* [BC BREAK] When used in the `prependExtension()` method, the `ContainerConfigurator::import()` method now prepends the configuration instead of appending it
1515
* Cast env vars to null or bool when referencing them using `#[Autowire(env: '...')]` depending on the signature of the corresponding parameter
16+
* Add `#[AutowireInline]` attribute to allow service definition at the class level
1617

1718
7.0
1819
---

Compiler/AutowirePass.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313

1414
use Symfony\Component\Config\Resource\ClassExistenceResource;
1515
use Symfony\Component\DependencyInjection\Attribute\Autowire;
16-
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;
1716
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
17+
use Symfony\Component\DependencyInjection\Attribute\AutowireInline;
1818
use Symfony\Component\DependencyInjection\Attribute\Lazy;
1919
use Symfony\Component\DependencyInjection\Attribute\Target;
2020
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -331,9 +331,9 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
331331
continue 2;
332332
}
333333

334-
if ($attribute instanceof AutowireCallable) {
334+
if ($attribute instanceof AutowireInline) {
335335
$value = $attribute->buildDefinition($value, $type, $parameter);
336-
$value = $this->doProcessValue($value);
336+
$value = new Reference('.autowire_inline.'.ContainerBuilder::hash($value));
337337
} elseif ($lazy = $attribute->lazy) {
338338
$definition = (new Definition($type))
339339
->setFactory('current')

Compiler/PassConfig.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public function __construct()
5555
new AutoAliasServicePass(),
5656
new ValidateEnvPlaceholdersPass(),
5757
new ResolveDecoratorStackPass(),
58+
new ResolveAutowireInlineAttributesPass(),
5859
new ResolveChildDefinitionsPass(),
5960
new RegisterServiceSubscribersPass(),
6061
new ResolveParameterPlaceHoldersPass(false, false),
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Attribute\AutowireInline;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Definition;
17+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
18+
use Symfony\Component\VarExporter\ProxyHelper;
19+
20+
/**
21+
* Inspects existing autowired services for {@see AutowireInline} attribute and registers the definitions for reuse.
22+
*
23+
* @author Ismail Özgün Turan <[email protected]>
24+
*/
25+
class ResolveAutowireInlineAttributesPass extends AbstractRecursivePass
26+
{
27+
protected bool $skipScalars = true;
28+
29+
protected function processValue(mixed $value, bool $isRoot = false): mixed
30+
{
31+
$value = parent::processValue($value, $isRoot);
32+
33+
if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
34+
return $value;
35+
}
36+
37+
try {
38+
$constructor = $this->getConstructor($value, false);
39+
} catch (RuntimeException) {
40+
$this->container->log($this, sprintf('Skipping service "%s": Class or interface "%s" cannot be loaded.', $this->currentId, $value->getClass()));
41+
42+
return $value;
43+
}
44+
45+
if ($constructor === null) {
46+
return $value;
47+
}
48+
49+
$reflectionParameters = $constructor->getParameters();
50+
foreach ($reflectionParameters as $reflectionParameter) {
51+
$autowireInlineAttributes = $reflectionParameter->getAttributes(AutowireInline::class, \ReflectionAttribute::IS_INSTANCEOF);
52+
foreach ($autowireInlineAttributes as $autowireInlineAttribute) {
53+
/** @var AutowireInline $autowireInlineAttributeInstance */
54+
$autowireInlineAttributeInstance = $autowireInlineAttribute->newInstance();
55+
56+
$type = ProxyHelper::exportType($reflectionParameter, true);
57+
$definition = $autowireInlineAttributeInstance->buildDefinition($autowireInlineAttributeInstance->value, $type, $reflectionParameter);
58+
59+
$this->container->setDefinition('.autowire_inline.'.ContainerBuilder::hash($definition), $definition);
60+
}
61+
}
62+
63+
return $value;
64+
}
65+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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\AutowireInline;
16+
use Symfony\Component\DependencyInjection\Reference;
17+
18+
class AutowireInlineTest extends TestCase
19+
{
20+
public function testInvalidFactoryArray()
21+
{
22+
$autowireInline = new AutowireInline([123, 456]);
23+
24+
self::assertSame([123, 456], $autowireInline->value['factory']);
25+
}
26+
27+
/**
28+
* @dataProvider provideInvalidCalls
29+
*/
30+
public function testInvalidCallsArray(array $calls)
31+
{
32+
$autowireInline = new AutowireInline('someClass', calls: $calls);
33+
34+
self::assertSame('someClass', $autowireInline->value['class']);
35+
self::assertSame($calls, $autowireInline->value['calls']);
36+
}
37+
38+
public static function provideInvalidCalls(): iterable
39+
{
40+
yield 'missing method' => [[[]]];
41+
yield 'invalid method value type1' => [[[null]]];
42+
yield 'invalid method value type2' => [[[123]]];
43+
yield 'invalid method value type3' => [[[true]]];
44+
yield 'invalid method value type4' => [[[false]]];
45+
yield 'invalid method value type5' => [[[new \stdClass()]]];
46+
yield 'invalid method value type6' => [[[[]]]];
47+
48+
yield 'invalid arguments value type1' => [[['someMethod', null]]];
49+
yield 'invalid arguments value type2' => [[['someMethod', 123]]];
50+
yield 'invalid arguments value type3' => [[['someMethod', true]]];
51+
yield 'invalid arguments value type4' => [[['someMethod', false]]];
52+
yield 'invalid arguments value type5' => [[['someMethod', new \stdClass()]]];
53+
yield 'invalid arguments value type6' => [[['someMethod', '']]];
54+
}
55+
56+
public function testClass()
57+
{
58+
$attribute = new AutowireInline('someClass');
59+
60+
$buildDefinition = $attribute->buildDefinition($attribute->value, null, $this->createReflectionParameter());
61+
62+
self::assertSame('someClass', $buildDefinition->getClass());
63+
self::assertSame([], $buildDefinition->getArguments());
64+
self::assertFalse($attribute->lazy);
65+
}
66+
67+
public function testClassAndParams()
68+
{
69+
$attribute = new AutowireInline('someClass', ['someParam']);
70+
71+
$buildDefinition = $attribute->buildDefinition($attribute->value, null, $this->createReflectionParameter());
72+
73+
self::assertSame('someClass', $buildDefinition->getClass());
74+
self::assertSame(['someParam'], $buildDefinition->getArguments());
75+
self::assertFalse($attribute->lazy);
76+
}
77+
78+
public function testClassAndParamsLazy()
79+
{
80+
$attribute = new AutowireInline('someClass', ['someParam'], lazy: true);
81+
82+
$buildDefinition = $attribute->buildDefinition($attribute->value, null, $this->createReflectionParameter());
83+
84+
self::assertSame('someClass', $buildDefinition->getClass());
85+
self::assertSame(['someParam'], $buildDefinition->getArguments());
86+
self::assertTrue($attribute->lazy);
87+
}
88+
89+
/**
90+
* @dataProvider provideFactories
91+
*/
92+
public function testFactory(string|array $factory, string|array $expectedResult)
93+
{
94+
$attribute = new AutowireInline($factory);
95+
96+
$buildDefinition = $attribute->buildDefinition($attribute->value, null, $this->createReflectionParameter());
97+
98+
self::assertNull($buildDefinition->getClass());
99+
self::assertEquals($expectedResult, $buildDefinition->getFactory());
100+
self::assertSame([], $buildDefinition->getArguments());
101+
self::assertFalse($attribute->lazy);
102+
}
103+
104+
/**
105+
* @dataProvider provideFactories
106+
*/
107+
public function testFactoryAndParams(string|array $factory, string|array $expectedResult)
108+
{
109+
$attribute = new AutowireInline($factory, ['someParam']);
110+
111+
$buildDefinition = $attribute->buildDefinition($attribute->value, null, $this->createReflectionParameter());
112+
113+
self::assertNull($buildDefinition->getClass());
114+
self::assertEquals($expectedResult, $buildDefinition->getFactory());
115+
self::assertSame(['someParam'], $buildDefinition->getArguments());
116+
self::assertFalse($attribute->lazy);
117+
}
118+
119+
/**
120+
* @dataProvider provideFactories
121+
*/
122+
public function testFactoryAndParamsLazy(string|array $factory, string|array $expectedResult)
123+
{
124+
$attribute = new AutowireInline($factory, ['someParam'], lazy: true);
125+
126+
$buildDefinition = $attribute->buildDefinition($attribute->value, null, $this->createReflectionParameter());
127+
128+
self::assertNull($buildDefinition->getClass());
129+
self::assertEquals($expectedResult, $buildDefinition->getFactory());
130+
self::assertSame(['someParam'], $buildDefinition->getArguments());
131+
self::assertTrue($attribute->lazy);
132+
}
133+
134+
public static function provideFactories(): iterable
135+
{
136+
yield 'string callable' => [[null, 'someFunction'], [null, 'someFunction']];
137+
138+
yield 'class only' => [['someClass'], ['someClass', '__invoke']];
139+
yield 'reference only' => [[new Reference('someClass')], [new Reference('someClass'), '__invoke']];
140+
141+
yield 'class with method' => [['someClass', 'someStaticMethod'], ['someClass', 'someStaticMethod']];
142+
yield 'reference with method' => [[new Reference('someClass'), 'someMethod'], [new Reference('someClass'), 'someMethod']];
143+
yield '@reference with method' => [['@someClass', 'someMethod'], [new Reference('someClass'), 'someMethod']];
144+
}
145+
146+
/**
147+
* @dataProvider provideCalls
148+
*/
149+
public function testCalls(string|array $calls, array $expectedResult)
150+
{
151+
$attribute = new AutowireInline('someClass', calls: $calls);
152+
153+
$buildDefinition = $attribute->buildDefinition($attribute->value, null, $this->createReflectionParameter());
154+
155+
self::assertSame('someClass', $buildDefinition->getClass());
156+
self::assertSame($expectedResult, $buildDefinition->getMethodCalls());
157+
self::assertSame([], $buildDefinition->getArguments());
158+
self::assertFalse($attribute->lazy);
159+
}
160+
161+
public static function provideCalls(): iterable
162+
{
163+
yield 'method with empty arguments' => [
164+
[['someMethod', []]],
165+
[['someMethod', []]],
166+
];
167+
yield 'method with arguments' => [
168+
[['someMethod', ['someArgument']]],
169+
[['someMethod', ['someArgument']]],
170+
];
171+
yield 'method without arguments with return clone true' => [
172+
[['someMethod', [], true]],
173+
[['someMethod', [], true]],
174+
];
175+
yield 'method without arguments with return clone false' => [
176+
[['someMethod', [], false]],
177+
[['someMethod', []]],
178+
];
179+
yield 'method with arguments with return clone true' => [
180+
[['someMethod', ['someArgument'], true]],
181+
[['someMethod', ['someArgument'], true]],
182+
];
183+
yield 'method with arguments with return clone false' => [
184+
[['someMethod', ['someArgument'], false]],
185+
[['someMethod', ['someArgument']]],
186+
];
187+
}
188+
189+
private function createReflectionParameter()
190+
{
191+
$class = new class('someValue') {
192+
public function __construct($someParameter)
193+
{
194+
}
195+
};
196+
$reflectionClass = new \ReflectionClass($class);
197+
198+
return $reflectionClass->getConstructor()->getParameters()[0];
199+
}
200+
}

0 commit comments

Comments
 (0)