Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0dface6

Browse files
[DependencyInjection] Add #[AutowireMethodOf] attribute to autowire a method of a service as a callable
1 parent 7e02b52 commit 0dface6

File tree

10 files changed

+105
-18
lines changed

10 files changed

+105
-18
lines changed
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\Attribute;
13+
14+
use Symfony\Component\DependencyInjection\Definition;
15+
use Symfony\Component\DependencyInjection\Reference;
16+
17+
/**
18+
* Tells which method should be turned into a Closure based on the name of the parameter it's attached to.
19+
*/
20+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
21+
class AutowireMethodOf extends AutowireCallable
22+
{
23+
/**
24+
* @param string $service The service containing the method to autowire
25+
* @param bool|class-string $lazy Whether to use lazy-loading for this argument
26+
*/
27+
public function __construct(string $service, bool|string $lazy = false)
28+
{
29+
parent::__construct([new Reference($service)], lazy: $lazy);
30+
}
31+
32+
public function buildDefinition(mixed $value, ?string $type, \ReflectionParameter $parameter): Definition
33+
{
34+
$value[1] = $parameter->name;
35+
36+
return parent::buildDefinition($value, $type, $parameter);
37+
}
38+
}

src/Symfony/Component/DependencyInjection/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* Add argument `$prepend` to `ContainerConfigurator::extension()` to prepend the configuration instead of appending it
99
* Have `ServiceLocator` implement `ServiceCollectionInterface`
1010
* Add `#[Lazy]` attribute as shortcut for `#[Autowire(lazy: [bool|string])]` and `#[Autoconfigure(lazy: [bool|string])]`
11+
* Add `#[AutowireMethodOf]` attribute to autowire a method of a service as a callable
1112

1213
7.0
1314
---

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
4747
if (!$value instanceof Reference) {
4848
return parent::processValue($value, $isRoot);
4949
}
50-
if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE < $value->getInvalidBehavior() || $this->container->has($id = (string) $value)) {
50+
if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE < $value->getInvalidBehavior() || $this->container->has((string) $value)) {
5151
return $value;
5252
}
5353

@@ -83,7 +83,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
8383
$this->throwServiceNotFoundException($value, $currentId, $value);
8484
}
8585

86-
private function throwServiceNotFoundException(Reference $ref, string $sourceId, $value): void
86+
private function throwServiceNotFoundException(Reference $ref, string $sourceId, mixed $value): void
8787
{
8888
$id = (string) $ref;
8989
$alternatives = [];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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\AutowireCallable;
16+
use Symfony\Component\DependencyInjection\Attribute\AutowireMethodOf;
17+
use Symfony\Component\DependencyInjection\Exception\LogicException;
18+
use Symfony\Component\DependencyInjection\Reference;
19+
20+
class AutowireMethodOfTest extends TestCase
21+
{
22+
public function testConstructor()
23+
{
24+
$a = new AutowireMethodOf('foo');
25+
26+
$this->assertEquals([new Reference('foo')], $a->value);
27+
}
28+
29+
public function testBuildDefinition(\Closure $dummy = null)
30+
{
31+
$a = new AutowireMethodOf('foo');
32+
$r = new \ReflectionParameter([__CLASS__, __FUNCTION__], 0);
33+
34+
$this->assertEquals([[new Reference('foo'), 'dummy']], $a->buildDefinition($a->value, 'Closure', $r)->getArguments());
35+
}
36+
}

src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ServiceValueResolver.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@ public function resolve(Request $request, ArgumentMetadata $argument): array
5454
try {
5555
return [$this->container->get($controller)->get($argument->getName())];
5656
} catch (RuntimeException $e) {
57-
$what = sprintf('argument $%s of "%s()"', $argument->getName(), $controller);
58-
$message = preg_replace('/service "\.service_locator\.[^"]++"/', $what, $e->getMessage());
57+
$what = 'argument $'.$argument->getName();
58+
$message = str_replace(sprintf('service "%s"', $argument->getName()), $what, $e->getMessage());
59+
$what .= sprintf(' of "%s()"', $controller);
60+
$message = preg_replace('/service "\.service_locator\.[^"]++"/', $what, $message);
5961

6062
if ($e->getMessage() === $message) {
6163
$message = sprintf('Cannot resolve %s: %s', $what, $message);

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ public function process(ContainerBuilder $container): void
120120

121121
// create a per-method map of argument-names to service/type-references
122122
$args = [];
123+
$erroredIds = 0;
123124
foreach ($parameters as $p) {
124125
/** @var \ReflectionParameter $p */
125126
$type = preg_replace('/(^|[(|&])\\\\/', '\1', $target = ltrim(ProxyHelper::exportType($p) ?? '', '?'));
@@ -168,10 +169,8 @@ public function process(ContainerBuilder $container): void
168169
$value = $parameterBag->resolveValue($attribute->value);
169170

170171
if ($attribute instanceof AutowireCallable) {
171-
$value = $attribute->buildDefinition($value, $type, $p);
172-
}
173-
174-
if ($value instanceof Reference) {
172+
$args[$p->name] = $attribute->buildDefinition($value, $type, $p);
173+
} elseif ($value instanceof Reference) {
175174
$args[$p->name] = $type ? new TypedReference($value, $type, $invalidBehavior, $p->name) : new Reference($value, $invalidBehavior);
176175
} else {
177176
$args[$p->name] = new Reference('.value.'.$container->hash($value));
@@ -195,14 +194,15 @@ public function process(ContainerBuilder $container): void
195194
->addError($message);
196195

197196
$args[$p->name] = new Reference($erroredId, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE);
197+
++$erroredIds;
198198
} else {
199199
$target = preg_replace('/(^|[(|&])\\\\/', '\1', $target);
200200
$args[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior, Target::parseName($p)) : new Reference($target, $invalidBehavior);
201201
}
202202
}
203203
// register the maps as a per-method service-locators
204204
if ($args) {
205-
$controllers[$id.'::'.$r->name] = ServiceLocatorTagPass::register($container, $args);
205+
$controllers[$id.'::'.$r->name] = ServiceLocatorTagPass::register($container, $args, \count($args) !== $erroredIds ? $id.'::'.$r->name.'()' : null);
206206

207207
foreach ($publicAliases[$id] ?? [] as $alias) {
208208
$controllers[$alias.'::'.$r->name] = clone $controllers[$id.'::'.$r->name];

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

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ public function process(ContainerBuilder $container): void
2929
foreach ($controllers as $controller => $argumentRef) {
3030
$argumentLocator = $container->getDefinition((string) $argumentRef->getValues()[0]);
3131

32+
if ($argumentLocator->getFactory()) {
33+
$argumentLocator = $container->getDefinition($argumentLocator->getFactory()[0]);
34+
}
35+
3236
if (!$argumentLocator->getArgument(0)) {
3337
// remove empty argument locators
3438
$reason = sprintf('Removing service-argument resolver for controller "%s": no corresponding services exist for the referenced types.', $controller);

src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public function testControllerNameIsAnArray()
8989
public function testErrorIsTruncated()
9090
{
9191
$this->expectException(RuntimeException::class);
92-
$this->expectExceptionMessage('Cannot autowire argument $dummy of "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyController::index()": it references class "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyService" but no such service exists.');
92+
$this->expectExceptionMessage('Cannot autowire argument $dummy required by "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyController::index()": it references class "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyService" but no such service exists.');
9393
$container = new ContainerBuilder();
9494
$container->addCompilerPass(new RegisterControllerArgumentLocatorsPass());
9595

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ public function testAllActions()
143143
$this->assertInstanceof(ServiceClosureArgument::class, $locator['foo::fooAction']);
144144

145145
$locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
146+
$locator = $container->getDefinition((string) $locator->getFactory()[0]);
146147

147148
$this->assertSame(ServiceLocator::class, $locator->getClass());
148149
$this->assertFalse($locator->isPublic());
@@ -166,6 +167,7 @@ public function testExplicitArgument()
166167

167168
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
168169
$locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
170+
$locator = $container->getDefinition((string) $locator->getFactory()[0]);
169171

170172
$expected = ['bar' => new ServiceClosureArgument(new TypedReference('bar', ControllerDummy::class, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE))];
171173
$this->assertEquals($expected, $locator->getArgument(0));
@@ -185,6 +187,7 @@ public function testOptionalArgument()
185187

186188
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
187189
$locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
190+
$locator = $container->getDefinition((string) $locator->getFactory()[0]);
188191

189192
$expected = ['bar' => new ServiceClosureArgument(new TypedReference('bar', ControllerDummy::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE))];
190193
$this->assertEquals($expected, $locator->getArgument(0));
@@ -306,8 +309,8 @@ public function testBindings($bindingName)
306309
$pass->process($container);
307310

308311
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
309-
310312
$locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
313+
$locator = $container->getDefinition((string) $locator->getFactory()[0]);
311314

312315
$expected = ['bar' => new ServiceClosureArgument(new Reference('foo'))];
313316
$this->assertEquals($expected, $locator->getArgument(0));
@@ -372,7 +375,8 @@ public function testBindingsOnChildDefinitions()
372375
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
373376
$this->assertInstanceOf(ServiceClosureArgument::class, $locator['child::fooAction']);
374377

375-
$locator = $container->getDefinition((string) $locator['child::fooAction']->getValues()[0])->getArgument(0);
378+
$locator = $container->getDefinition((string) $locator['child::fooAction']->getValues()[0]);
379+
$locator = $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0);
376380
$this->assertInstanceOf(ServiceClosureArgument::class, $locator['someArg']);
377381
$this->assertEquals(new Reference('parent'), $locator['someArg']->getValues()[0]);
378382
}
@@ -439,6 +443,7 @@ public function testBindWithTarget()
439443

440444
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
441445
$locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
446+
$locator = $container->getDefinition((string) $locator->getFactory()[0]);
442447

443448
$expected = [
444449
'apiKey' => new ServiceClosureArgument(new Reference('the_api_key')),

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

+7-6
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,23 @@ public function testProcess()
3535
$pass->process($container);
3636

3737
$controllers = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
38+
$getLocator = fn ($controllers, $k) => $container->getDefinition((string) $container->getDefinition((string) $controllers[$k]->getValues()[0])->getFactory()[0])->getArgument(0);
3839

39-
$this->assertCount(2, $container->getDefinition((string) $controllers['c1::fooAction']->getValues()[0])->getArgument(0));
40-
$this->assertCount(1, $container->getDefinition((string) $controllers['c2::setTestCase']->getValues()[0])->getArgument(0));
41-
$this->assertCount(1, $container->getDefinition((string) $controllers['c2::fooAction']->getValues()[0])->getArgument(0));
40+
$this->assertCount(2, $getLocator($controllers, 'c1::fooAction'));
41+
$this->assertCount(1, $getLocator($controllers, 'c2::setTestCase'));
42+
$this->assertCount(1, $getLocator($controllers, 'c2::fooAction'));
4243

4344
(new ResolveInvalidReferencesPass())->process($container);
4445

45-
$this->assertCount(1, $container->getDefinition((string) $controllers['c2::setTestCase']->getValues()[0])->getArgument(0));
46-
$this->assertSame([], $container->getDefinition((string) $controllers['c2::fooAction']->getValues()[0])->getArgument(0));
46+
$this->assertCount(1, $getLocator($controllers, 'c2::setTestCase'));
47+
$this->assertSame([], $getLocator($controllers, 'c2::fooAction'));
4748

4849
(new RemoveEmptyControllerArgumentLocatorsPass())->process($container);
4950

5051
$controllers = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
5152

5253
$this->assertSame(['c1::fooAction', 'c1:fooAction'], array_keys($controllers));
53-
$this->assertSame(['bar'], array_keys($container->getDefinition((string) $controllers['c1::fooAction']->getValues()[0])->getArgument(0)));
54+
$this->assertSame(['bar'], array_keys($getLocator($controllers, 'c1::fooAction')));
5455

5556
$expectedLog = [
5657
'Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass: Removing service-argument resolver for controller "c2::fooAction": no corresponding services exist for the referenced types.',

0 commit comments

Comments
 (0)