Skip to content

Commit 95fa158

Browse files
feature #51832 [DependencyInjection] Add #[AutowireIterator] attribute and improve #[AutowireLocator] (nicolas-grekas, kbond)
This PR was merged into the 6.4 branch. Discussion ---------- [DependencyInjection] Add `#[AutowireIterator]` attribute and improve `#[AutowireLocator]` | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT This PR is building on #51392 to add an `#[AutowireIterator]` attribute and improve `#[AutowireLocator]`. The new `#[AutowireIterator]` attribute can be used to describe what `#[AutowireLocator]` can do, except that we get an iterator instead of a container. And `#[AutowireLocator]` can now be used instead of `#[TaggedLocator]`: `#[AutowireLocator('foo')]` and done. In order to describe that you want a list of services, we cannot use named arguments anymore so we have to pass an array now: `#[AutowireLocator(['foo' => 'F', 'bar' => 'B'])]` should be used instead of `#[AutowireLocator(foo: 'F', bar: 'B')]`. Last but not least, this adds support for nesting `SubscribedService` objects in the list of described services. This provides feature-parity with what we can do when implementing `ServiceSubscriberInterface`. I didn't deprecate `TaggedIterator` nor `TaggedLocator`. We could, but maybe we should wait for 7.1? TODO: - [x] add tests - thanks `@kbond`! ~PS: while writing this, I realize that we may merge both tags in one, and let `AutowirePass` decide if it should build a locator or an iterator based on the type of the argument that has the attribute. We'd "just" need to find a name that'd work for that.~ Commits ------- a87f2e0 [DependencyInjection] Add tests for `AutowireLocator`/`AutowireIterator` 78018de [DependencyInjection] Add `#[AutowireIterator]` attribute and improve `#[AutowireLocator]`
2 parents d27190a + a87f2e0 commit 95fa158

14 files changed

+239
-61
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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\Argument\IteratorArgument;
15+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
16+
use Symfony\Component\DependencyInjection\ContainerInterface;
17+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
18+
use Symfony\Component\DependencyInjection\TypedReference;
19+
use Symfony\Contracts\Service\Attribute\SubscribedService;
20+
use Symfony\Contracts\Service\ServiceSubscriberInterface;
21+
22+
/**
23+
* Autowires an iterator of services based on a tag name or an explicit list of key => service-type pairs.
24+
*/
25+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
26+
class AutowireIterator extends Autowire
27+
{
28+
/**
29+
* @see ServiceSubscriberInterface::getSubscribedServices()
30+
*
31+
* @param string|array<string|SubscribedService> $services A tag name or an explicit list of services
32+
* @param string|string[] $exclude A service or a list of services to exclude
33+
*/
34+
public function __construct(
35+
string|array $services,
36+
string $indexAttribute = null,
37+
string $defaultIndexMethod = null,
38+
string $defaultPriorityMethod = null,
39+
string|array $exclude = [],
40+
bool $excludeSelf = true,
41+
) {
42+
if (\is_string($services)) {
43+
parent::__construct(new TaggedIteratorArgument($services, $indexAttribute, $defaultIndexMethod, false, $defaultPriorityMethod, (array) $exclude, $excludeSelf));
44+
45+
return;
46+
}
47+
48+
$references = [];
49+
50+
foreach ($services as $key => $type) {
51+
$attributes = [];
52+
53+
if ($type instanceof SubscribedService) {
54+
$key = $type->key ?? $key;
55+
$attributes = $type->attributes;
56+
$type = ($type->nullable ? '?' : '').($type->type ?? throw new InvalidArgumentException(sprintf('When "%s" is used, a type must be set.', SubscribedService::class)));
57+
}
58+
59+
if (!\is_string($type) || !preg_match('/(?(DEFINE)(?<cn>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+))(?(DEFINE)(?<fqcn>(?&cn)(?:\\\\(?&cn))*+))^\??(?&fqcn)(?:(?:\|(?&fqcn))*+|(?:&(?&fqcn))*+)$/', $type)) {
60+
throw new InvalidArgumentException(sprintf('"%s" is not a PHP type for key "%s".', \is_string($type) ? $type : get_debug_type($type), $key));
61+
}
62+
$optionalBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
63+
if ('?' === $type[0]) {
64+
$type = substr($type, 1);
65+
$optionalBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
66+
}
67+
if (\is_int($name = $key)) {
68+
$key = $type;
69+
$name = null;
70+
}
71+
72+
$references[$key] = new TypedReference($type, $type, $optionalBehavior, $name, $attributes);
73+
}
74+
75+
parent::__construct(new IteratorArgument($references));
76+
}
77+
}

src/Symfony/Component/DependencyInjection/Attribute/AutowireLocator.php

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,40 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Attribute;
1313

14+
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1415
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
15-
use Symfony\Component\DependencyInjection\ContainerInterface;
16-
use Symfony\Component\DependencyInjection\Reference;
16+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
17+
use Symfony\Contracts\Service\Attribute\SubscribedService;
18+
use Symfony\Contracts\Service\ServiceSubscriberInterface;
1719

20+
/**
21+
* Autowires a service locator based on a tag name or an explicit list of key => service-type pairs.
22+
*/
1823
#[\Attribute(\Attribute::TARGET_PARAMETER)]
1924
class AutowireLocator extends Autowire
2025
{
21-
public function __construct(string ...$serviceIds)
22-
{
23-
$values = [];
24-
25-
foreach ($serviceIds as $key => $serviceId) {
26-
if ($nullable = str_starts_with($serviceId, '?')) {
27-
$serviceId = substr($serviceId, 1);
28-
}
29-
30-
if (is_numeric($key)) {
31-
$key = $serviceId;
32-
}
33-
34-
$values[$key] = new Reference(
35-
$serviceId,
36-
$nullable ? ContainerInterface::IGNORE_ON_INVALID_REFERENCE : ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE,
37-
);
26+
/**
27+
* @see ServiceSubscriberInterface::getSubscribedServices()
28+
*
29+
* @param string|array<string|SubscribedService> $services An explicit list of services or a tag name
30+
* @param string|string[] $exclude A service or a list of services to exclude
31+
*/
32+
public function __construct(
33+
string|array $services,
34+
string $indexAttribute = null,
35+
string $defaultIndexMethod = null,
36+
string $defaultPriorityMethod = null,
37+
string|array $exclude = [],
38+
bool $excludeSelf = true,
39+
) {
40+
$iterator = (new AutowireIterator($services, $indexAttribute, $defaultIndexMethod, $defaultPriorityMethod, (array) $exclude, $excludeSelf))->value;
41+
42+
if ($iterator instanceof TaggedIteratorArgument) {
43+
$iterator = new TaggedIteratorArgument($iterator->getTag(), $iterator->getIndexAttribute(), $iterator->getDefaultIndexMethod(), true, $iterator->getDefaultPriorityMethod(), $iterator->getExclude(), $iterator->excludeSelf());
44+
} elseif ($iterator instanceof IteratorArgument) {
45+
$iterator = $iterator->getValues();
3846
}
3947

40-
parent::__construct(new ServiceLocatorArgument($values));
48+
parent::__construct(new ServiceLocatorArgument($iterator));
4149
}
4250
}

src/Symfony/Component/DependencyInjection/Attribute/TaggedIterator.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Attribute;
1313

14-
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
15-
1614
#[\Attribute(\Attribute::TARGET_PARAMETER)]
17-
class TaggedIterator extends Autowire
15+
class TaggedIterator extends AutowireIterator
1816
{
1917
public function __construct(
2018
public string $tag,
@@ -24,6 +22,6 @@ public function __construct(
2422
public string|array $exclude = [],
2523
public bool $excludeSelf = true,
2624
) {
27-
parent::__construct(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, false, $defaultPriorityMethod, (array) $exclude, $excludeSelf));
25+
parent::__construct($tag, $indexAttribute, $defaultIndexMethod, $defaultPriorityMethod, $exclude, $excludeSelf);
2826
}
2927
}

src/Symfony/Component/DependencyInjection/Attribute/TaggedLocator.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,8 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Attribute;
1313

14-
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
15-
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
16-
1714
#[\Attribute(\Attribute::TARGET_PARAMETER)]
18-
class TaggedLocator extends Autowire
15+
class TaggedLocator extends AutowireLocator
1916
{
2017
public function __construct(
2118
public string $tag,
@@ -25,6 +22,6 @@ public function __construct(
2522
public string|array $exclude = [],
2623
public bool $excludeSelf = true,
2724
) {
28-
parent::__construct(new ServiceLocatorArgument(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, true, $defaultPriorityMethod, (array) $exclude, $excludeSelf)));
25+
parent::__construct($tag, $indexAttribute, $defaultIndexMethod, $defaultPriorityMethod, $exclude, $excludeSelf);
2926
}
3027
}

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ CHANGELOG
77
* Allow using `#[Target]` with no arguments to state that a parameter must match a named autowiring alias
88
* Deprecate `ContainerAwareInterface` and `ContainerAwareTrait`, use dependency injection instead
99
* Add `defined` env var processor that returns `true` for defined and neither null nor empty env vars
10-
* Add `#[AutowireLocator]` attribute
10+
* Add `#[AutowireLocator]` and `#[AutowireIterator]` attributes
1111

1212
6.3
1313
---

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,16 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
7979
$attributes = [];
8080

8181
if ($type instanceof SubscribedService) {
82-
$key = $type->key;
82+
$key = $type->key ?? $key;
8383
$attributes = $type->attributes;
8484
$type = ($type->nullable ? '?' : '').($type->type ?? throw new InvalidArgumentException(sprintf('When "%s::getSubscribedServices()" returns "%s", a type must be set.', $class, SubscribedService::class)));
8585
}
8686

8787
if (!\is_string($type) || !preg_match('/(?(DEFINE)(?<cn>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+))(?(DEFINE)(?<fqcn>(?&cn)(?:\\\\(?&cn))*+))^\??(?&fqcn)(?:(?:\|(?&fqcn))*+|(?:&(?&fqcn))*+)$/', $type)) {
8888
throw new InvalidArgumentException(sprintf('"%s::getSubscribedServices()" must return valid PHP types for service "%s" key "%s", "%s" returned.', $class, $this->currentId, $key, \is_string($type) ? $type : get_debug_type($type)));
8989
}
90-
if ($optionalBehavior = '?' === $type[0]) {
90+
$optionalBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
91+
if ('?' === $type[0]) {
9192
$type = substr($type, 1);
9293
$optionalBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
9394
}
@@ -120,7 +121,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
120121
$name = $this->container->has($type.' $'.$camelCaseName) ? $camelCaseName : $name;
121122
}
122123

123-
$subscriberMap[$key] = new TypedReference((string) $serviceMap[$key], $type, $optionalBehavior ?: ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $name, $attributes);
124+
$subscriberMap[$key] = new TypedReference((string) $serviceMap[$key], $type, $optionalBehavior, $name, $attributes);
124125
unset($serviceMap[$key]);
125126
}
126127

src/Symfony/Component/DependencyInjection/Tests/Attribute/AutowireLocatorTest.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,33 @@
1515
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
1616
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
1717
use Symfony\Component\DependencyInjection\ContainerInterface;
18-
use Symfony\Component\DependencyInjection\Reference;
18+
use Symfony\Component\DependencyInjection\TypedReference;
1919

2020
class AutowireLocatorTest extends TestCase
2121
{
2222
public function testSimpleLocator()
2323
{
24-
$locator = new AutowireLocator('foo', 'bar');
24+
$locator = new AutowireLocator(['foo', 'bar']);
2525

2626
$this->assertEquals(
27-
new ServiceLocatorArgument(['foo' => new Reference('foo'), 'bar' => new Reference('bar')]),
27+
new ServiceLocatorArgument(['foo' => new TypedReference('foo', 'foo'), 'bar' => new TypedReference('bar', 'bar')]),
2828
$locator->value,
2929
);
3030
}
3131

3232
public function testComplexLocator()
3333
{
34-
$locator = new AutowireLocator(
34+
$locator = new AutowireLocator([
3535
'?qux',
36-
foo: 'bar',
37-
bar: '?baz',
38-
);
36+
'foo' => 'bar',
37+
'bar' => '?baz',
38+
]);
3939

4040
$this->assertEquals(
4141
new ServiceLocatorArgument([
42-
'qux' => new Reference('qux', ContainerInterface::IGNORE_ON_INVALID_REFERENCE),
43-
'foo' => new Reference('bar'),
44-
'bar' => new Reference('baz', ContainerInterface::IGNORE_ON_INVALID_REFERENCE),
42+
'qux' => new TypedReference('qux', 'qux', ContainerInterface::IGNORE_ON_INVALID_REFERENCE),
43+
'foo' => new TypedReference('bar', 'bar', name: 'foo'),
44+
'bar' => new TypedReference('baz', 'baz', ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'bar'),
4545
]),
4646
$locator->value,
4747
);

src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Psr\Container\ContainerInterface;
1616
use Symfony\Component\Config\FileLocator;
1717
use Symfony\Component\DependencyInjection\Alias;
18+
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
1819
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
1920
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
2021
use Symfony\Component\DependencyInjection\ChildDefinition;
@@ -32,6 +33,7 @@
3233
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfiguredInterface2;
3334
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfiguredService1;
3435
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfiguredService2;
36+
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutowireIteratorConsumer;
3537
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutowireLocatorConsumer;
3638
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarTagClass;
3739
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedClass;
@@ -392,6 +394,7 @@ public function testTaggedServiceWithIndexAttributeAndDefaultMethod()
392394
public function testLocatorConfiguredViaAttribute()
393395
{
394396
$container = new ContainerBuilder();
397+
$container->setParameter('some.parameter', 'foo');
395398
$container->register(BarTagClass::class)
396399
->setPublic(true)
397400
;
@@ -411,6 +414,36 @@ public function testLocatorConfiguredViaAttribute()
411414
self::assertSame($container->get(BarTagClass::class), $s->locator->get(BarTagClass::class));
412415
self::assertSame($container->get(FooTagClass::class), $s->locator->get('with_key'));
413416
self::assertFalse($s->locator->has('nullable'));
417+
self::assertSame('foo', $s->locator->get('subscribed'));
418+
}
419+
420+
public function testIteratorConfiguredViaAttribute()
421+
{
422+
$container = new ContainerBuilder();
423+
$container->setParameter('some.parameter', 'foo');
424+
$container->register(BarTagClass::class)
425+
->setPublic(true)
426+
;
427+
$container->register(FooTagClass::class)
428+
->setPublic(true)
429+
;
430+
$container->register(AutowireIteratorConsumer::class)
431+
->setAutowired(true)
432+
->setPublic(true)
433+
;
434+
435+
$container->compile();
436+
437+
/** @var AutowireIteratorConsumer $s */
438+
$s = $container->get(AutowireIteratorConsumer::class);
439+
440+
self::assertInstanceOf(RewindableGenerator::class, $s->iterator);
441+
442+
$values = iterator_to_array($s->iterator);
443+
self::assertCount(3, $values);
444+
self::assertSame($container->get(BarTagClass::class), $values[BarTagClass::class]);
445+
self::assertSame($container->get(FooTagClass::class), $values['with_key']);
446+
self::assertSame('foo', $values['subscribed']);
414447
}
415448

416449
public function testTaggedServiceWithIndexAttributeAndDefaultMethodConfiguredViaAttribute()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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\Fixtures;
13+
14+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
15+
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
16+
use Symfony\Contracts\Service\Attribute\SubscribedService;
17+
18+
final class AutowireIteratorConsumer
19+
{
20+
public function __construct(
21+
#[AutowireIterator([
22+
BarTagClass::class,
23+
'with_key' => FooTagClass::class,
24+
'nullable' => '?invalid',
25+
'subscribed' => new SubscribedService(type: 'string', attributes: new Autowire('%some.parameter%')),
26+
])]
27+
public readonly iterable $iterator,
28+
) {
29+
}
30+
}

src/Symfony/Component/DependencyInjection/Tests/Fixtures/AutowireLocatorConsumer.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,19 @@
1212
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
1313

1414
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
1516
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
17+
use Symfony\Contracts\Service\Attribute\SubscribedService;
1618

1719
final class AutowireLocatorConsumer
1820
{
1921
public function __construct(
20-
#[AutowireLocator(
22+
#[AutowireLocator([
2123
BarTagClass::class,
22-
with_key: FooTagClass::class,
23-
nullable: '?invalid',
24-
)]
24+
'with_key' => FooTagClass::class,
25+
'nullable' => '?invalid',
26+
'subscribed' => new SubscribedService(type: 'string', attributes: new Autowire('%some.parameter%')),
27+
])]
2528
public readonly ContainerInterface $locator,
2629
) {
2730
}

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