Skip to content

Commit b332e24

Browse files
feature #54016 [DependencyInjection] Add #[AutowireMethodOf] attribute to autowire a method of a service as a callable (nicolas-grekas)
This PR was merged into the 7.1 branch. Discussion ---------- [DependencyInjection] Add `#[AutowireMethodOf]` attribute to autowire a method of a service as a callable | Q | A | ------------- | --- | Branch? | 7.1 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT Let's take a controller for example, with one action that has this argument: ```php #[AutowireMethodOf(CommentRepository::class)] \Closure $getCommentPaginator, ``` The proposed attribute tells Symfony to create a closure that will call `CommentRepository::getCommentPaginator()`. The name of the method to be called is inferred from the name of the parameter. This is already doable with this syntax, so that the proposed attribute is just a shortcut for this: ```php #[AutowireCallable(service: CommentRepository::class, method: 'getCommentPaginator')] \Closure $getCommentPaginator, ``` Using this style allows turning e.g. entity repositories into query functions, which are way more flexible. But because the existing syntax is quite verbose, i looked for a more concise alternative, so here we are with this proposal. Benefits: - Increased Flexibility: Allows developers to inject specific methods as Closures, providing greater control over what functionality is injected into - Improved Readability: By using the attribute, the intention behind the dependency injection is made explicit, improving code clarity. - **Enhanced Modularity: Facilitates a more modular architecture by decoupling services from direct dependencies on specific class methods, making the codebase more maintainable and testable.** Because we leverage the existing code infrastructure for AutowireCallable, if I declare this interface: ```php interface GetCommentPaginatorInterface { public function __invoke(Conference $conference, int $page): Paginator; } ``` then I can also do native types (vs a closure) without doing anything else: ```php #[AutowireMethodOf(CommentRepository::class)] GetCommentPaginatorInterface $getCommentPaginator, ``` Commits ------- df11660015 [DependencyInjection] Add `#[AutowireMethodOf]` attribute to autowire a method of a service as a callable
2 parents 09b6621 + 4499030 commit b332e24

File tree

4 files changed

+75
-2
lines changed

4 files changed

+75
-2
lines changed

Attribute/AutowireMethodOf.php

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+
}

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

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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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\AutowireMethodOf;
16+
use Symfony\Component\DependencyInjection\Reference;
17+
18+
class AutowireMethodOfTest extends TestCase
19+
{
20+
public function testConstructor()
21+
{
22+
$a = new AutowireMethodOf('foo');
23+
24+
$this->assertEquals([new Reference('foo')], $a->value);
25+
}
26+
27+
public function testBuildDefinition(?\Closure $dummy = null)
28+
{
29+
$a = new AutowireMethodOf('foo');
30+
$r = new \ReflectionParameter([__CLASS__, __FUNCTION__], 0);
31+
32+
$this->assertEquals([[new Reference('foo'), 'dummy']], $a->buildDefinition($a->value, 'Closure', $r)->getArguments());
33+
}
34+
}

0 commit comments

Comments
 (0)