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
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\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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 2 additions & 2 deletions
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 = [];
Lines changed: 36 additions & 0 deletions
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 <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\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

Lines changed: 4 additions & 2 deletions
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

Lines changed: 5 additions & 5 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 7 additions & 2 deletions
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

Lines changed: 7 additions & 6 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy