Skip to content

Commit a7bdd96

Browse files
feature #22200 [DI] Reference tagged services in config (ro0NL)
This PR was merged into the 3.4 branch. Discussion ---------- [DI] Reference tagged services in config | Q | A | ------------- | --- | Branch? | 3.4 | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #12269 | License | MIT | Doc PR | symfony/symfony-docs#8404 This is a proof of concept to reference a sequence of tagged services. The problem bugs me for some time, and at first i thought the solution was to have some super generic compiler pass. If it could replace a lot of compilers in core.. perhaps worth it, but eventually each tag comes with it's own logic, including how to deal with tag attributes. However, writing the passes over and over again becomes tedious for the most basic usecase. So given the recent developments, this idea came to mind. ```yml services: a: class: stdClass properties: { a: true } tags: [foo] b: class: stdClass properties: { b: true } tags: [foo] c: class: stdClass properties: #stds: !tagged_services foo (see #22198) stds: !tagged_services foo ``` ``` dump(iterator_to_array($this->get('c')->stds)); ``` ``` array:2 [▼ 0 => {#5052 ▼ +"a": true } 1 => {#4667 ▼ +"b": true } ] ``` Given the _basic_ example at https://symfony.com/doc/current/service_container/tags.html, this could replace that. Any thoughts? Commits ------- 979e58f [DI] Reference tagged services in config
2 parents 284b808 + 5bb3c71 commit a7bdd96

25 files changed

+267
-13
lines changed

Argument/TaggedIteratorArgument.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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\Argument;
13+
14+
/**
15+
* Represents a collection of services found by tag name to lazily iterate over.
16+
*
17+
* @author Roland Franssen <franssen.roland@gmail.com>
18+
*/
19+
class TaggedIteratorArgument extends IteratorArgument
20+
{
21+
private $tag;
22+
23+
/**
24+
* @param string $tag
25+
*/
26+
public function __construct($tag)
27+
{
28+
parent::__construct(array());
29+
30+
$this->tag = (string) $tag;
31+
}
32+
33+
public function getTag()
34+
{
35+
return $this->tag;
36+
}
37+
}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ CHANGELOG
1313
* deprecated support for top-level anonymous services in XML
1414
* deprecated case insensitivity of parameter names
1515
* deprecated the `ResolveDefinitionTemplatesPass` class in favor of `ResolveChildDefinitionsPass`
16+
* added `TaggedIteratorArgument` with YAML (`!tagged foo`) and XML (`<service type="tagged"/>`) support
1617

1718
3.3.0
1819
-----

Compiler/PassConfig.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public function __construct()
6161
new AutowireRequiredMethodsPass(),
6262
new ResolveBindingsPass(),
6363
new AutowirePass(false),
64+
new ResolveTaggedIteratorArgumentPass(),
6465
new ResolveServiceSubscribersPass(),
6566
new ResolveReferencesToAliasesPass(),
6667
new ResolveInvalidReferencesPass(),
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\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
15+
16+
/**
17+
* Resolves all TaggedIteratorArgument arguments.
18+
*
19+
* @author Roland Franssen <franssen.roland@gmail.com>
20+
*/
21+
class ResolveTaggedIteratorArgumentPass extends AbstractRecursivePass
22+
{
23+
use PriorityTaggedServiceTrait;
24+
25+
/**
26+
* {@inheritdoc}
27+
*/
28+
protected function processValue($value, $isRoot = false)
29+
{
30+
if (!$value instanceof TaggedIteratorArgument) {
31+
return parent::processValue($value, $isRoot);
32+
}
33+
34+
$value->setValues($this->findAndSortTaggedServices($value->getTag(), $this->container));
35+
36+
return $value;
37+
}
38+
}

Dumper/XmlDumper.php

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

1414
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1515
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
16+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
1617
use Symfony\Component\DependencyInjection\ContainerInterface;
1718
use Symfony\Component\DependencyInjection\Parameter;
1819
use Symfony\Component\DependencyInjection\Reference;
@@ -298,6 +299,9 @@ private function convertParameters(array $parameters, $type, \DOMElement $parent
298299
if (is_array($value)) {
299300
$element->setAttribute('type', 'collection');
300301
$this->convertParameters($value, $type, $element, 'key');
302+
} elseif ($value instanceof TaggedIteratorArgument) {
303+
$element->setAttribute('type', 'tagged');
304+
$element->setAttribute('tag', $value->getTag());
301305
} elseif ($value instanceof IteratorArgument) {
302306
$element->setAttribute('type', 'iterator');
303307
$this->convertParameters($value->getValues(), $type, $element, 'key');

Dumper/YamlDumper.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
2020
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
2121
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
22+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
2223
use Symfony\Component\DependencyInjection\ContainerInterface;
2324
use Symfony\Component\DependencyInjection\Definition;
2425
use Symfony\Component\DependencyInjection\Parameter;
@@ -263,6 +264,9 @@ private function dumpValue($value)
263264
$value = $value->getValues()[0];
264265
}
265266
if ($value instanceof ArgumentInterface) {
267+
if ($value instanceof TaggedIteratorArgument) {
268+
return new TaggedValue('tagged', $value->getTag());
269+
}
266270
if ($value instanceof IteratorArgument) {
267271
$tag = 'iterator';
268272
} else {

Loader/Configurator/ContainerConfigurator.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

1414
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
15+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
1516
use Symfony\Component\DependencyInjection\ContainerBuilder;
1617
use Symfony\Component\DependencyInjection\Definition;
1718
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
@@ -115,6 +116,18 @@ function iterator(array $values)
115116
return new IteratorArgument(AbstractConfigurator::processValue($values, true));
116117
}
117118

119+
/**
120+
* Creates a lazy iterator by tag name.
121+
*
122+
* @param string $tag
123+
*
124+
* @return TaggedIteratorArgument
125+
*/
126+
function tagged($tag)
127+
{
128+
return new TaggedIteratorArgument($tag);
129+
}
130+
118131
/**
119132
* Creates an expression.
120133
*

Loader/XmlFileLoader.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Loader;
1313

1414
use Symfony\Component\Config\Util\XmlUtils;
15+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
1516
use Symfony\Component\DependencyInjection\ContainerInterface;
1617
use Symfony\Component\DependencyInjection\Alias;
1718
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
@@ -518,6 +519,12 @@ private function getArgumentsAsPhp(\DOMElement $node, $name, $file, $lowercase =
518519
throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="iterator" only accepts collections of type="service" references in "%s".', $name, $file));
519520
}
520521
break;
522+
case 'tagged':
523+
if (!$arg->getAttribute('tag')) {
524+
throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="tagged" has no or empty "tag" attribute in "%s".', $name, $file));
525+
}
526+
$arguments[$key] = new TaggedIteratorArgument($arg->getAttribute('tag'));
527+
break;
521528
case 'string':
522529
$arguments[$key] = $arg->nodeValue;
523530
break;

Loader/YamlFileLoader.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
1616
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
1717
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
18+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
1819
use Symfony\Component\DependencyInjection\ChildDefinition;
1920
use Symfony\Component\DependencyInjection\ContainerBuilder;
2021
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -726,6 +727,13 @@ private function resolveServices($value, $file, $isParameter = false)
726727
throw new InvalidArgumentException(sprintf('"!iterator" tag only accepts arrays of "@service" references in "%s".', $file));
727728
}
728729
}
730+
if ('tagged' === $value->getTag()) {
731+
if (!is_string($argument) || !$argument) {
732+
throw new InvalidArgumentException(sprintf('"!tagged" tag only accepts non empty string in "%s".', $file));
733+
}
734+
735+
return new TaggedIteratorArgument($argument);
736+
}
729737
if ('service' === $value->getTag()) {
730738
if ($isParameter) {
731739
throw new InvalidArgumentException(sprintf('Using an anonymous service in a parameter is not allowed in "%s".', $file));

Loader/schema/dic/services/services-1.0.xsd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
<xsd:attribute name="name" type="xsd:string" />
209209
<xsd:attribute name="on-invalid" type="invalid_sequence" />
210210
<xsd:attribute name="strict" type="boolean" />
211+
<xsd:attribute name="tag" type="xsd:string" />
211212
</xsd:complexType>
212213

213214
<xsd:complexType name="bind" mixed="true">
@@ -233,6 +234,7 @@
233234
<xsd:attribute name="index" type="xsd:integer" />
234235
<xsd:attribute name="on-invalid" type="invalid_sequence" />
235236
<xsd:attribute name="strict" type="boolean" />
237+
<xsd:attribute name="tag" type="xsd:string" />
236238
</xsd:complexType>
237239

238240
<xsd:complexType name="call">
@@ -258,6 +260,7 @@
258260
<xsd:enumeration value="string" />
259261
<xsd:enumeration value="constant" />
260262
<xsd:enumeration value="iterator" />
263+
<xsd:enumeration value="tagged" />
261264
</xsd:restriction>
262265
</xsd:simpleType>
263266

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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\Compiler;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
16+
use Symfony\Component\DependencyInjection\Compiler\ResolveTaggedIteratorArgumentPass;
17+
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
use Symfony\Component\DependencyInjection\Reference;
19+
20+
/**
21+
* @author Roland Franssen <franssen.roland@gmail.com>
22+
*/
23+
class ResolveTaggedIteratorArgumentPassTest extends TestCase
24+
{
25+
public function testProcess()
26+
{
27+
$container = new ContainerBuilder();
28+
$container->register('a', 'stdClass')->addTag('foo');
29+
$container->register('b', 'stdClass')->addTag('foo', array('priority' => 20));
30+
$container->register('c', 'stdClass')->addTag('foo', array('priority' => 10));
31+
$container->register('d', 'stdClass')->setProperty('foos', new TaggedIteratorArgument('foo'));
32+
33+
(new ResolveTaggedIteratorArgumentPass())->process($container);
34+
35+
$properties = $container->getDefinition('d')->getProperties();
36+
$expected = new TaggedIteratorArgument('foo');
37+
$expected->setValues(array(new Reference('b'), new Reference('c'), new Reference('a')));
38+
$this->assertEquals($expected, $properties['foos']);
39+
}
40+
}

Tests/Fixtures/config/basic.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
use App\BarService;
66

77
return function (ContainerConfigurator $c) {
8-
98
$s = $c->services();
109
$s->set(BarService::class)
1110
->args(array(inline('FooClass')));
12-
1311
};

Tests/Fixtures/config/child.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use App\BarService;
66

77
return function (ContainerConfigurator $c) {
8-
98
$c->services()
109
->set('bar', 'Class1')
1110
->set(BarService::class)
@@ -20,5 +19,4 @@
2019
->parent('bar')
2120
->parent(BarService::class)
2221
;
23-
2422
};

Tests/Fixtures/config/defaults.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo;
66

77
return function (ContainerConfigurator $c) {
8-
98
$c->import('basic.php');
109

1110
$s = $c->services()->defaults()
@@ -19,5 +18,4 @@
1918

2019
$s->set(Foo::class)->args(array(ref('bar')))->public();
2120
$s->set('bar', Foo::class)->call('setFoo')->autoconfigure(false);
22-
2321
};

Tests/Fixtures/config/instanceof.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
77

88
return function (ContainerConfigurator $c) {
9-
109
$s = $c->services();
1110
$s->instanceof(Prototype\Foo::class)
1211
->property('p', 0)
@@ -20,5 +19,4 @@
2019
$s->load(Prototype::class.'\\', '../Prototype')->exclude('../Prototype/*/*');
2120

2221
$s->set('foo', FooService::class);
23-
2422
};

Tests/Fixtures/config/php7.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo;
66

77
return function (ContainerConfigurator $c) {
8-
98
$c->parameters()
109
('foo', 'Foo')
1110
('bar', 'Bar')
@@ -17,5 +16,4 @@
1716
('bar', Foo::class)
1817
->call('setFoo')
1918
;
20-
2119
};

Tests/Fixtures/config/prototype.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
66

77
return function (ContainerConfigurator $c) {
8-
98
$di = $c->services()->defaults()
109
->tag('baz');
1110
$di->load(Prototype::class.'\\', '../Prototype')
@@ -20,5 +19,4 @@
2019
->parent('foo');
2120
$di->set('foo')->lazy()->abstract();
2221
$di->get(Prototype\Foo::class)->lazy(false);
23-
2422
};

Tests/Fixtures/config/services9.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
require_once __DIR__.'/../includes/foo.php';
1010

1111
return function (ContainerConfigurator $c) {
12-
1312
$p = $c->parameters();
1413
$p->set('baz_class', 'BazClass');
1514
$p->set('foo_class', FooClass::class)
@@ -119,4 +118,11 @@
119118
$s->set('lazy_context_ignore_invalid_ref', 'LazyContext')
120119
->args(array(iterator(array(ref('foo.baz'), ref('invalid')->ignoreOnInvalid())), iterator(array())));
121120

121+
$s->set('tagged_iterator_foo', 'Bar')
122+
->private()
123+
->tag('foo');
124+
125+
$s->set('tagged_iterator', 'Bar')
126+
->public()
127+
->args(array(tagged('foo')));
122128
};

Tests/Fixtures/containers/container9.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require_once __DIR__.'/../includes/foo.php';
55

66
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
7+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
78
use Symfony\Component\DependencyInjection\ContainerInterface;
89
use Symfony\Component\DependencyInjection\ContainerBuilder;
910
use Symfony\Component\DependencyInjection\Reference;
@@ -161,5 +162,15 @@
161162
->setArguments(array(new IteratorArgument(array(new Reference('foo.baz'), new Reference('invalid', ContainerInterface::IGNORE_ON_INVALID_REFERENCE))), new IteratorArgument(array())))
162163
->setPublic(true)
163164
;
165+
$container
166+
->register('tagged_iterator_foo', 'Bar')
167+
->addTag('foo')
168+
->setPublic(false)
169+
;
170+
$container
171+
->register('tagged_iterator', 'Bar')
172+
->addArgument(new TaggedIteratorArgument('foo'))
173+
->setPublic(true)
174+
;
164175

165176
return $container;

Tests/Fixtures/graphviz/services9.dot

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ digraph sc {
2929
node_factory_service_simple [label="factory_service_simple\nBar\n", shape=record, fillcolor="#eeeeee", style="filled"];
3030
node_lazy_context [label="lazy_context\nLazyContext\n", shape=record, fillcolor="#eeeeee", style="filled"];
3131
node_lazy_context_ignore_invalid_ref [label="lazy_context_ignore_invalid_ref\nLazyContext\n", shape=record, fillcolor="#eeeeee", style="filled"];
32+
node_tagged_iterator_foo [label="tagged_iterator_foo\nBar\n", shape=record, fillcolor="#eeeeee", style="filled"];
33+
node_tagged_iterator [label="tagged_iterator\nBar\n", shape=record, fillcolor="#eeeeee", style="filled"];
3234
node_foo2 [label="foo2\n\n", shape=record, fillcolor="#ff9999", style="filled"];
3335
node_foo3 [label="foo3\n\n", shape=record, fillcolor="#ff9999", style="filled"];
3436
node_foobaz [label="foobaz\n\n", shape=record, fillcolor="#ff9999", style="filled"];

0 commit comments

Comments
 (0)