8000 [DependencyInjection] Add `#[AutowireIterator]` attribute and improve… · symfony/symfony@78018de · GitHub
[go: up one dir, main page]

Skip to content

Commit 78018de

Browse files
[DependencyInjection] Add #[AutowireIterator] attribute and improve #[AutowireLocator]
1 parent 7be1c03 commit 78018de

File tree

9 files changed

+129
-48
lines changed

9 files changed

+129
-48
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/Fixtures/AutowireLocatorConsumer.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
< D250 code>1717
final class AutowireLocatorConsumer
1818
{
1919
public function __construct(
20-
#[AutowireLocator(
20+
#[AutowireLocator([
2121
BarTagClass::class,
22-
with_key: FooTagClass::class,
23-
nullable: '?invalid',
24-
)]
22+
'with_key' => FooTagClass::class,
23+
'nullable' => '?invalid',
24+
])]
2525
public readonly ContainerInterface $locator,
2626
) {
2727
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,7 @@ class WithTaggedIteratorAndTaggedLocator
676676
public function fooAction(
677677
#[TaggedIterator('foobar')] iterable $iterator,
678678
#[TaggedLocator('foobar')] ServiceLocator $locator,
679-
#[AutowireLocator('bar', 'baz')] ContainerInterface $container,
679+
#[AutowireLocator(['bar', 'baz'])] ContainerInterface $container,
680680
) {
681681
}
682682
}

0 commit comments

Comments
 (0)
0