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

Skip to content

Commit b4f6c34

Browse files
feature #30257 [DependencyInjection] Allow to choose an index for tagged collection (deguif, XuruDragon)
This PR was merged into the 4.3-dev branch. Discussion ---------- [DependencyInjection] Allow to choose an index for tagged collection | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #29203 | License | MIT | Doc PR | symfony/symfony-docs#11009 This is the continuity of the PR #29598 Add a way to specify an index based on a tag attribute when injecting a tag collection into services, but also a a way to fallback to a static method on the service class. ```yaml services: foo_service: class: Foo tags: - foo foo_service_tagged: class: Bar arguments: - !tagged tag: 'foo' index_by: 'tag_attribute_name' default_index_method: 'static_method' ``` ```xml <?xml version="1.0" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <service id="foo" class="Foo"> <tag name="foo_tag" /> </service> <service id="foo_tagged_iterator" class="Bar" public="true"> <argument type="tagged" tag="foo_tag" index-by="tag_attribute_name" default-index-method="static_method" /> </service> </services> </container> ``` Tasks * [x] Support PHP loader/dumper * [x] Support YAML loader/dumper * [x] Support XML loader/dumper (and update XSD too) * [x] Add tests * [x] Documentation Commits ------- 101bfd7 [DI] change name to tag + add XMl support + adding yaml/xml tests 845d3a6 Allow to choose an index for tagged collection
2 parents fec0475 + 101bfd7 commit b4f6c34

20 files changed

+297
-18
lines changed

src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,40 @@
1919
class TaggedIteratorArgument extends IteratorArgument
2020
{
2121
private $tag;
22+
private $indexAttribute;
23+
private $defaultIndexMethod;
2224

23-
public function __construct(string $tag)
25+
/**
26+
* TaggedIteratorArgument constructor.
27+
*
28+
* @param string $tag The name of the tag identifying the target services
29+
* @param string|null $indexAttribute The name of the attribute that defines the key referencing each service in the tagged collection
30+
* @param string|null $defaultIndexMethod The static method that should be called to get each service's key when their tag doesn't define the previous attribute
31+
*/
32+
public function __construct(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null)
2433
{
2534
parent::__construct([]);
2635

2736
$this->tag = $tag;
37+
38+
if ($indexAttribute) {
39+
$this->indexAttribute = $indexAttribute;
40+
$this->defaultIndexMethod = $defaultIndexMethod ?: ('getDefault'.str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $indexAttribute))).'Name');
41+
}
2842
}
2943

3044
public function getTag()
3145
{
3246
return $this->tag;
3347
}
48+
49+
public function getIndexAttribute(): ?string
50+
{
51+
return $this->indexAttribute;
52+
}
53+
54+
public function getDefaultIndexMethod(): ?string
55+
{
56+
return $this->defaultIndexMethod;
57+
}
3458
}

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* added `%env(nullable:...)%` processor to allow empty variables to be processed as null values
1010
* added support for deprecating aliases
1111
* made `ContainerParametersResource` final and not implement `Serializable` anymore
12+
* added ability to define an index for a tagged collection
1213

1314
4.2.0
1415
-----

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

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Compiler;
1313

14+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
1415
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1517
use Symfony\Component\DependencyInjection\Reference;
1618

1719
/**
@@ -31,18 +33,59 @@ trait PriorityTaggedServiceTrait
3133
* @see https://bugs.php.net/bug.php?id=53710
3234
* @see https://bugs.php.net/bug.php?id=60926
3335
*
34-
* @param string $tagName
35-
* @param ContainerBuilder $container
36+
* @param string|TaggedIteratorArgument $tagName
37+
* @param ContainerBuilder $container
3638
*
3739
* @return Reference[]
3840
*/
3941
private function findAndSortTaggedServices($tagName, ContainerBuilder $container)
4042
{
43+
$indexAttribute = $defaultIndexMethod = null;
44+
if ($tagName instanceof TaggedIteratorArgument) {
45+
$indexAttribute = $tagName->getIndexAttribute();
46+
$defaultIndexMethod = $tagName->getDefaultIndexMethod();
47+
$tagName = $tagName->getTag();
48+
}
4149
$services = [];
4250

4351
foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $attributes) {
4452
$priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
45-
$services[$priority][] = new Reference($serviceId);
53+
54+
if (null === $indexAttribute) {
55+
$services[$priority][] = new Reference($serviceId);
56+
57+
continue;
58+
}
59+
60+
if (isset($attributes[0][$indexAttribute])) {
61+
$services[$priority][$attributes[0][$indexAttribute]] = new Reference($serviceId);
62+
63+
continue;
64+
}
65+
66+
if (!$r = $container->getReflectionClass($class = $container->getDefinition($serviceId)->getClass())) {
67+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $serviceId));
68+
}
69+
70+
if (!$r->hasMethod($defaultIndexMethod)) {
71+
throw new InvalidArgumentException(sprintf('Method "%s::%s()" not found: tag "%s" on service "%s" is missing "%s" attribute.', $class, $defaultIndexMethod, $tagName, $serviceId, $indexAttribute));
72+
}
73+
74+
if (!($rm = $r->getMethod($defaultIndexMethod))->isStatic()) {
75+
throw new InvalidArgumentException(sprintf('Method "%s::%s()" should be static: tag "%s" on service "%s" is missing "%s" attribute.', $class, $defaultIndexMethod, $tagName, $serviceId, $indexAttribute));
76+
}
77+
78+
if (!$rm->isPublic()) {
79+
throw new InvalidArgumentException(sprintf('Method "%s::%s()" should be public: tag "%s" on service "%s" is missing "%s" attribute.', $class, $defaultIndexMethod, $tagName, $serviceId, $indexAttribute));
80+
}
81+
82+
$key = $rm->invoke(null);
83+
84+
if (!\is_string($key)) {
85+
throw new InvalidArgumentException(sprintf('Method "%s::%s()" should return a string, got %s: tag "%s" on service "%s" is missing "%s" attribute.', $class, $defaultIndexMethod, \gettype($key), $tagName, $serviceId, $indexAttribute));
86+
}
87+
88+
$services[$priority][$key] = new Reference($serviceId);
4689
}
4790

4891
if ($services) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ protected function processValue($value, $isRoot = false)
3131
return parent::processValue($value, $isRoot);
3232
}
3333

34-
$value->setValues($this->findAndSortTaggedServices($value->getTag(), $this->container));
34+
$value->setValues($this->findAndSortTaggedServices($value, $this->container));
3535

3636
return $value;
3737
}

src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,14 @@ private function convertParameters(array $parameters, $type, \DOMElement $parent
286286
} elseif ($value instanceof TaggedIteratorArgument) {
287287
$element->setAttribute('type', 'tagged');
288288
$element->setAttribute('tag', $value->getTag());
289+
290+
if (null !== $value->getIndexAttribute()) {
291+
$element->setAttribute('index-by', $value->getIndexAttribute());
292+
}
293+
294+
if (null !== $value->getDefaultIndexMethod()) {
295+
$element->setAttribute('default-index-method', $value->getDefaultIndexMethod());
296+
}
289297
} elseif ($value instanceof IteratorArgument) {
290298
$element->setAttribute('type', 'iterator');
291299
$this->convertParameters($value->getValues(), $type, $element, 'key');

src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,19 @@ private function dumpValue($value)
233233
}
234234
if ($value instanceof ArgumentInterface) {
235235
if ($value instanceof TaggedIteratorArgument) {
236+
if (null !== $value->getIndexAttribute()) {
237+
$taggedValueContent = [
238+
'tag' => $value->getTag(),
239+
'index_by' => $value->getIndexAttribute(),
240+
];
241+
242+
if (null !== $value->getDefaultIndexMethod()) {
243+
$taggedValueContent['default_index_method'] = $value->getDefaultIndexMethod();
244+
}
245+
246+
return new TaggedValue('tagged', $taggedValueContent);
247+
}
248+
236249
return new TaggedValue('tagged', $value->getTag());
237250
}
238251
if ($value instanceof IteratorArgument) {

src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,9 @@ function iterator(array $values): IteratorArgument
116116
/**
117117
* Creates a lazy iterator by tag name.
118118
*/
119-
function tagged(string $tag): TaggedIteratorArgument
119+
function tagged(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null): TaggedIteratorArgument
120120
{
121-
return new TaggedIteratorArgument($tag);
121+
return new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod);
122122
}
123123

124124
/**

src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults)
353353
continue;
354354
}
355355

356-
if (false !== strpos($name, '-') && false === strpos($name, '_') && !array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) {
356+
if (false !== strpos($name, '-') && false === strpos($name, '_') && !\array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) {
357357
$parameters[$normalizedName] = XmlUtils::phpize($node->nodeValue);
358358
}
359359
// keep not normalized key
@@ -537,7 +537,8 @@ private function getArgumentsAsPhp(\DOMElement $node, $name, $file, $lowercase =
537537
if (!$arg->getAttribute('tag')) {
538538
throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="tagged" has no or empty "tag" attribute in "%s".', $name, $file));
539539
}
540-
$arguments[$key] = new TaggedIteratorArgument($arg->getAttribute('tag'));
540+
541+
$arguments[$key] = new TaggedIteratorArgument($arg->getAttribute('tag'), $arg->getAttribute('index-by') ?: null, $arg->getAttribute('default-index-method') ?: null);
541542
break;
542543
case 'binary':
543544
if (false === $value = base64_decode($arg->nodeValue)) {

src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ private function parseDefinitions(array $content, string $file)
203203
throw new InvalidArgumentException(sprintf('The "services" key should contain an array in %s. Check your YAML syntax.', $file));
204204
}
205205

206-
if (array_key_exists('_instanceof', $content['services'])) {
206+
if (\array_key_exists('_instanceof', $content['services'])) {
207207
$instanceof = $content['services']['_instanceof'];
208208
unset($content['services']['_instanceof']);
209209

@@ -235,7 +235,7 @@ private function parseDefinitions(array $content, string $file)
235235
*/
236236
private function parseDefaults(array &$content, string $file): array
237237
{
238-
if (!array_key_exists('_defaults', $content['services'])) {
238+
if (!\array_key_exists('_defaults', $content['services'])) {
239239
return [];
240240
}
241241
$defaults = $content['services']['_defaults'];
@@ -342,7 +342,7 @@ private function parseDefinition($id, $service, $file, array $defaults)
342342

343343
if (isset($service['alias'])) {
344344
$this->container->setAlias($id, $alias = new Alias($service['alias']));
345-
if (array_key_exists('public', $service)) {
345+
if (\array_key_exists('public', $service)) {
346346
$alias->setPublic($service['public']);
347347
} elseif (isset($defaults['public'])) {
348348
$alias->setPublic($defaults['public']);
@@ -430,7 +430,7 @@ private function parseDefinition($id, $service, $file, array $defaults)
430430
$definition->setAbstract($service['abstract']);
431431
}
432432

433-
if (array_key_exists('deprecated', $service)) {
433+
if (\array_key_exists('deprecated', $service)) {
434434
$definition->setDeprecated(true, $service['deprecated']);
435435
}
436436

@@ -545,11 +545,11 @@ private function parseDefinition($id, $service, $file, array $defaults)
545545
}
546546
}
547547

548-
if (array_key_exists('namespace', $service) && !array_key_exists('resource', $service)) {
548+
if (\array_key_exists('namespace', $service) && !\array_key_exists('resource', $service)) {
549549
throw new InvalidArgumentException(sprintf('A "resource" attribute must be set when the "namespace" attribute is set for service "%s" in %s. Check your YAML syntax.', $id, $file));
550550
}
551551

552-
if (array_key_exists('resource', $service)) {
552+
if (\array_key_exists('resource', $service)) {
553553
if (!\is_string($service['resource'])) {
554554
throw new InvalidArgumentException(sprintf('A "resource" attribute must be of type string for service "%s" in %s. Check your YAML syntax.', $id, $file));
555555
}
@@ -710,11 +710,19 @@ private function resolveServices($value, $file, $isParameter = false)
710710
}
711711
}
712712
if ('tagged' === $value->getTag()) {
713-
if (!\is_string($argument) || !$argument) {
714-
throw new InvalidArgumentException(sprintf('"!tagged" tag only accepts non empty string in "%s".', $file));
713+
if (\is_string($argument) && $argument) {
714+
return new TaggedIteratorArgument($argument);
715715
}
716716

717-
return new TaggedIteratorArgument($argument);
717+
if (\is_array($argument) && isset($argument['tag']) && $argument['tag']) {
718+
if ($diff = array_diff(array_keys($argument), ['tag', 'index_by', 'default_index_method'])) {
719+
throw new InvalidArgumentException(sprintf('"!tagged" tag contains unsupported key "%s"; supported ones are "tag", "index_by" and "default_index_method".', implode('"", "', $diff)));
720+
}
721+
722+
return new TaggedIteratorArgument($argument['tag'], $argument['index_by'] ?? null, $argument['default_index_method'] ?? null);
723+
}
724+
725+
throw new InvalidArgumentException(sprintf('"!tagged" tags only accept a non empty string or an array with a key "tag" in "%s".', $file));
718726
}
719727
if ('service' === $value->getTag()) {
720728
if ($isParameter) {

src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@
234234
<xsd:attribute name="index" type="xsd:integer" />
235235
<xsd:attribute name="on-invalid" type="invalid_sequence" />
236236
<xsd:attribute name="tag" type="xsd:string" />
237+
<xsd:attribute name="index-by" type="xsd:string" />
238+
<xsd:attribute name="default-index-method" type="xsd:string" />
237239
</xsd:complexType>
238240

239241
<xsd:complexType name="call">

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Config\FileLocator;
1616
use Symfony\Component\DependencyInjection\Alias;
17+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
1718
use Symfony\Component\DependencyInjection\ContainerBuilder;
1819
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
1920
use Symfony\Component\DependencyInjection\Reference;
2021
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
22+
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarTagClass;
23+
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedClass;
24+
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooTagClass;
2125

2226
/**
2327
* This class tests the integration of the different compiler passes.
@@ -234,6 +238,54 @@ public function getYamlCompileTests()
234238
$container,
235239
];
236240
}
241+
242+
public function testTaggedServiceWithIndexAttribute()
243+
{
244+
$container = new ContainerBuilder();
245+
$container->register(BarTagClass::class, BarTagClass::class)
246+
->setPublic(true)
247+
->addTag('foo_bar', ['foo' => 'bar'])
248+
;
249+
$container->register(FooTagClass::class, FooTagClass::class)
250+
->setPublic(true)
251+
->addTag('foo_bar')
252+
;
253+
$container->register(FooBarTaggedClass::class, FooBarTaggedClass::class)
254+
->addArgument(new TaggedIteratorArgument('foo_bar', 'foo'))
255+
->setPublic(true)
256+
;
257+
258+
$container->compile();
259+
260+
$s = $container->get(FooBarTaggedClass::class);
261+
262+
$param = iterator_to_array($s->getParam()->getIterator());
263+
$this->assertSame(['bar' => $container->get(BarTagClass::class), 'foo_tag_class' => $container->get(FooTagClass::class)], $param);
264+
}
265+
266+
public function testTaggedServiceWithIndexAttributeAndDefaultMethod()
267+
{
268+
$container = new ContainerBuilder();
269+
$container->register(BarTagClass::class, BarTagClass::class)
270+
->setPublic(true)
271+
->addTag('foo_bar')
272+
;
273+
$container->register(FooTagClass::class, FooTagClass::class)
274+
->setPublic(true)
275+
->addTag('foo_bar', ['foo' => 'foo'])
276+
;
277+
$container->register(FooBarTaggedClass::class, FooBarTaggedClass::class)
278+
->addArgument(new TaggedIteratorArgument('foo_bar', 'foo', 'getFooBar'))
279+
->setPublic(true)
280+
;
281+
282+
$container->compile();
283+
284+
$s = $container->get(FooBarTaggedClass::class);
285+
286+
$param = iterator_to_array($s->getParam()->getIterator());
287+
$this->assertSame(['bar_tab_class_with_defaultmethod' => $container->get(BarTagClass::class), 'foo' => $container->get(FooTagClass::class)], $param);
288+
}
237289
}
238290

239291
class ServiceSubscriberStub implements ServiceSubscriberInterface

src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Config\FileLocator;
16+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
1617
use Symfony\Component\DependencyInjection\ContainerBuilder;
1718
use Symfony\Component\DependencyInjection\ContainerInterface;
1819
use Symfony\Component\DependencyInjection\Dumper\XmlDumper;
@@ -200,6 +201,19 @@ public function testDumpLoad()
200201
$this->assertStringEqualsFile(self::$fixturesPath.'/xml/services_dump_load.xml', $dumper->dump());
201202
}
202203

204+
public function testTaggedArgument()
205+
{
206+
$container = new ContainerBuilder();
207+
$container->register('foo', 'Foo')->addTag('foo_tag');
208+
$container->register('foo_tagged_iterator', 'Bar')
209+
->setPublic(true)
210+
->addArgument(new TaggedIteratorArgument('foo_tag', 'barfoo', 'foobar'))
211+
;
212+
213+
$dumper = new XmlDumper($container);
214+
$this->assertStringEqualsFile(self::$fixturesPath.'/xml/services_with_tagged_arguments.xml', $dumper->dump());
215+
}
216+
203217
public function testDumpAbstractServices()
204218
{
205219
$container = include self::$fixturesPath.'/containers/container_abstract.php';

0 commit comments

Comments
 (0)