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        | https://github.com/symfony/symfony-docs/issues/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
This commit is contained in:
Nicolas Grekas 2017-09-28 17:24:12 +02:00
commit 648a8953dd
27 changed files with 271 additions and 15 deletions

View File

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Argument;
/**
* Represents a collection of services found by tag name to lazily iterate over.
*
* @author Roland Franssen <franssen.roland@gmail.com>
*/
class TaggedIteratorArgument extends IteratorArgument
{
private $tag;
/**
* @param string $tag
*/
public function __construct($tag)
{
parent::__construct(array());
$this->tag = (string) $tag;
}
public function getTag()
{
return $this->tag;
}
}

View File

@ -13,6 +13,7 @@ CHANGELOG
* deprecated support for top-level anonymous services in XML
* deprecated case insensitivity of parameter names
* deprecated the `ResolveDefinitionTemplatesPass` class in favor of `ResolveChildDefinitionsPass`
* added `TaggedIteratorArgument` with YAML (`!tagged foo`) and XML (`<service type="tagged"/>`) support
3.3.0
-----

View File

@ -61,6 +61,7 @@ class PassConfig
new AutowireRequiredMethodsPass(),
new ResolveBindingsPass(),
new AutowirePass(false),
new ResolveTaggedIteratorArgumentPass(),
new ResolveServiceSubscribersPass(),
new ResolveReferencesToAliasesPass(),
new ResolveInvalidReferencesPass(),

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
/**
* Resolves all TaggedIteratorArgument arguments.
*
* @author Roland Franssen <franssen.roland@gmail.com>
*/
class ResolveTaggedIteratorArgumentPass extends AbstractRecursivePass
{
use PriorityTaggedServiceTrait;
/**
* {@inheritdoc}
*/
protected function processValue($value, $isRoot = false)
{
if (!$value instanceof TaggedIteratorArgument) {
return parent::processValue($value, $isRoot);
}
$value->setValues($this->findAndSortTaggedServices($value->getTag(), $this->container));
return $value;
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Dumper;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\Reference;
@ -298,6 +299,9 @@ class XmlDumper extends Dumper
if (is_array($value)) {
$element->setAttribute('type', 'collection');
$this->convertParameters($value, $type, $element, 'key');
} elseif ($value instanceof TaggedIteratorArgument) {
$element->setAttribute('type', 'tagged');
$element->setAttribute('tag', $value->getTag());
} elseif ($value instanceof IteratorArgument) {
$element->setAttribute('type', 'iterator');
$this->convertParameters($value->getValues(), $type, $element, 'key');

View File

@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Parameter;
@ -263,6 +264,9 @@ class YamlDumper extends Dumper
$value = $value->getValues()[0];
}
if ($value instanceof ArgumentInterface) {
if ($value instanceof TaggedIteratorArgument) {
return new TaggedValue('tagged', $value->getTag());
}
if ($value instanceof IteratorArgument) {
$tag = 'iterator';
} else {

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
@ -115,6 +116,18 @@ function iterator(array $values)
return new IteratorArgument(AbstractConfigurator::processValue($values, true));
}
/**
* Creates a lazy iterator by tag name.
*
* @param string $tag
*
* @return TaggedIteratorArgument
*/
function tagged($tag)
{
return new TaggedIteratorArgument($tag);
}
/**
* Creates an expression.
*

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
@ -518,6 +519,12 @@ class XmlFileLoader extends FileLoader
throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="iterator" only accepts collections of type="service" references in "%s".', $name, $file));
}
break;
case 'tagged':
if (!$arg->getAttribute('tag')) {
throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="tagged" has no or empty "tag" attribute in "%s".', $name, $file));
}
$arguments[$key] = new TaggedIteratorArgument($arg->getAttribute('tag'));
break;
case 'string':
$arguments[$key] = $arg->nodeValue;
break;

View File

@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -726,6 +727,13 @@ class YamlFileLoader extends FileLoader
throw new InvalidArgumentException(sprintf('"!iterator" tag only accepts arrays of "@service" references in "%s".', $file));
}
}
if ('tagged' === $value->getTag()) {
if (!is_string($argument) || !$argument) {
throw new InvalidArgumentException(sprintf('"!tagged" tag only accepts non empty string in "%s".', $file));
}
return new TaggedIteratorArgument($argument);
}
if ('service' === $value->getTag()) {
if ($isParameter) {
throw new InvalidArgumentException(sprintf('Using an anonymous service in a parameter is not allowed in "%s".', $file));

View File

@ -208,6 +208,7 @@
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="on-invalid" type="invalid_sequence" />
<xsd:attribute name="strict" type="boolean" />
<xsd:attribute name="tag" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="bind" mixed="true">
@ -233,6 +234,7 @@
<xsd:attribute name="index" type="xsd:integer" />
<xsd:attribute name="on-invalid" type="invalid_sequence" />
<xsd:attribute name="strict" type="boolean" />
<xsd:attribute name="tag" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="call">
@ -258,6 +260,7 @@
<xsd:enumeration value="string" />
<xsd:enumeration value="constant" />
<xsd:enumeration value="iterator" />
<xsd:enumeration value="tagged" />
</xsd:restriction>
</xsd:simpleType>

View File

@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\ResolveTaggedIteratorArgumentPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Roland Franssen <franssen.roland@gmail.com>
*/
class ResolveTaggedIteratorArgumentPassTest extends TestCase
{
public function testProcess()
{
$container = new ContainerBuilder();
$container->register('a', 'stdClass')->addTag('foo');
$container->register('b', 'stdClass')->addTag('foo', array('priority' => 20));
$container->register('c', 'stdClass')->addTag('foo', array('priority' => 10));
$container->register('d', 'stdClass')->setProperty('foos', new TaggedIteratorArgument('foo'));
(new ResolveTaggedIteratorArgumentPass())->process($container);
$properties = $container->getDefinition('d')->getProperties();
$expected = new TaggedIteratorArgument('foo');
$expected->setValues(array(new Reference('b'), new Reference('c'), new Reference('a')));
$this->assertEquals($expected, $properties['foos']);
}
}

View File

@ -5,9 +5,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\BarService;
return function (ContainerConfigurator $c) {
$s = $c->services();
$s->set(BarService::class)
->args(array(inline('FooClass')));
};

View File

@ -5,7 +5,6 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\BarService;
return function (ContainerConfigurator $c) {
$c->services()
->set('bar', 'Class1')
->set(BarService::class)
@ -20,5 +19,4 @@ return function (ContainerConfigurator $c) {
->parent('bar')
->parent(BarService::class)
;
};

View File

@ -5,7 +5,6 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo;
return function (ContainerConfigurator $c) {
$c->import('basic.php');
$s = $c->services()->defaults()
@ -19,5 +18,4 @@ return function (ContainerConfigurator $c) {
$s->set(Foo::class)->args(array(ref('bar')))->public();
$s->set('bar', Foo::class)->call('setFoo')->autoconfigure(false);
};

View File

@ -6,7 +6,6 @@ use App\FooService;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
return function (ContainerConfigurator $c) {
$s = $c->services();
$s->instanceof(Prototype\Foo::class)
->property('p', 0)
@ -20,5 +19,4 @@ return function (ContainerConfigurator $c) {
$s->load(Prototype::class.'\\', '../Prototype')->exclude('../Prototype/*/*');
$s->set('foo', FooService::class);
};

View File

@ -5,7 +5,6 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo;
return function (ContainerConfigurator $c) {
$c->parameters()
('foo', 'Foo')
('bar', 'Bar')
@ -17,5 +16,4 @@ return function (ContainerConfigurator $c) {
('bar', Foo::class)
->call('setFoo')
;
};

View File

@ -5,7 +5,6 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
return function (ContainerConfigurator $c) {
$di = $c->services()->defaults()
->tag('baz');
$di->load(Prototype::class.'\\', '../Prototype')
@ -20,5 +19,4 @@ return function (ContainerConfigurator $c) {
->parent('foo');
$di->set('foo')->lazy()->abstract();
$di->get(Prototype\Foo::class)->lazy(false);
};

View File

@ -9,7 +9,6 @@ require_once __DIR__.'/../includes/classes.php';
require_once __DIR__.'/../includes/foo.php';
return function (ContainerConfigurator $c) {
$p = $c->parameters();
$p->set('baz_class', 'BazClass');
$p->set('foo_class', FooClass::class)
@ -119,4 +118,11 @@ return function (ContainerConfigurator $c) {
$s->set('lazy_context_ignore_invalid_ref', 'LazyContext')
->args(array(iterator(array(ref('foo.baz'), ref('invalid')->ignoreOnInvalid())), iterator(array())));
$s->set('tagged_iterator_foo', 'Bar')
->private()
->tag('foo');
$s->set('tagged_iterator', 'Bar')
->public()
->args(array(tagged('foo')));
};

View File

@ -4,6 +4,7 @@ require_once __DIR__.'/../includes/classes.php';
require_once __DIR__.'/../includes/foo.php';
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
@ -161,5 +162,15 @@ $container
->setArguments(array(new IteratorArgument(array(new Reference('foo.baz'), new Reference('invalid', ContainerInterface::IGNORE_ON_INVALID_REFERENCE))), new IteratorArgument(array())))
->setPublic(true)
;
$container
->register('tagged_iterator_foo', 'Bar')
->addTag('foo')
->setPublic(false)
;
$container
->register('tagged_iterator', 'Bar')
->addArgument(new TaggedIteratorArgument('foo'))
->setPublic(true)
;
return $container;

View File

@ -29,6 +29,8 @@ digraph sc {
node_factory_service_simple [label="factory_service_simple\nBar\n", shape=record, fillcolor="#eeeeee", style="filled"];
node_lazy_context [label="lazy_context\nLazyContext\n", shape=record, fillcolor="#eeeeee", style="filled"];
node_lazy_context_ignore_invalid_ref [label="lazy_context_ignore_invalid_ref\nLazyContext\n", shape=record, fillcolor="#eeeeee", style="filled"];
node_tagged_iterator_foo [label="tagged_iterator_foo\nBar\n", shape=record, fillcolor="#eeeeee", style="filled"];
node_tagged_iterator [label="tagged_iterator\nBar\n", shape=record, fillcolor="#eeeeee", style="filled"];
node_foo2 [label="foo2\n\n", shape=record, fillcolor="#ff9999", style="filled"];
node_foo3 [label="foo3\n\n", shape=record, fillcolor="#ff9999", style="filled"];
node_foobaz [label="foobaz\n\n", shape=record, fillcolor="#ff9999", style="filled"];

View File

@ -56,6 +56,8 @@ class ProjectServiceContainer extends Container
'new_factory' => 'getNewFactoryService',
'new_factory_service' => 'getNewFactoryServiceService',
'service_from_static_method' => 'getServiceFromStaticMethodService',
'tagged_iterator' => 'getTaggedIteratorService',
'tagged_iterator_foo' => 'getTaggedIteratorFooService',
);
$this->privates = array(
'configurator_service' => true,
@ -63,6 +65,7 @@ class ProjectServiceContainer extends Container
'factory_simple' => true,
'inlined' => true,
'new_factory' => true,
'tagged_iterator_foo' => true,
);
$this->aliases = array(
'Psr\\Container\\ContainerInterface' => 'service_container',
@ -337,6 +340,18 @@ class ProjectServiceContainer extends Container
return $this->services['service_from_static_method'] = \Bar\FooClass::getInstance();
}
/**
* Gets the public 'tagged_iterator' shared service.
*
* @return \Bar
*/
protected function getTaggedIteratorService()
{
return $this->services['tagged_iterator'] = new \Bar(new RewindableGenerator(function () {
return new \EmptyIterator();
}, 0));
}
/**
* Gets the private 'configurator_service' shared service.
*
@ -404,6 +419,16 @@ class ProjectServiceContainer extends Container
return $instance;
}
/**
* Gets the private 'tagged_iterator_foo' shared service.
*
* @return \Bar
*/
protected function getTaggedIteratorFooService()
{
return $this->services['tagged_iterator_foo'] = new \Bar();
}
/**
* Gets the default parameters.
*

View File

@ -235,6 +235,27 @@ use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
return $this->services['service_from_static_method'] = \Bar\FooClass::getInstance();
[Container%s/getTaggedIteratorService.php] => <?php
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
// This file has been auto-generated by the Symfony Dependency Injection Component for internal use.
// Returns the public 'tagged_iterator' shared service.
return $this->services['tagged_iterator'] = new \Bar(new RewindableGenerator(function () {
yield 0 => ${($_ = isset($this->services['foo']) ? $this->services['foo'] : $this->load(__DIR__.'/getFooService.php')) && false ?: '_'};
yield 1 => ${($_ = isset($this->services['tagged_iterator_foo']) ? $this->services['tagged_iterator_foo'] : $this->services['tagged_iterator_foo'] = new \Bar()) && false ?: '_'};
}, 2));
[Container%s/getTaggedIteratorFooService.php] => <?php
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
// This file has been auto-generated by the Symfony Dependency Injection Component for internal use.
// Returns the private 'tagged_iterator_foo' shared service.
return $this->services['tagged_iterator_foo'] = new \Bar();
[Container%s/Container.php] => <?php
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
@ -292,9 +313,12 @@ class Container%s extends Container
'method_call1' => __DIR__.'/getMethodCall1Service.php',
'new_factory_service' => __DIR__.'/getNewFactoryServiceService.php',
'service_from_static_method' => __DIR__.'/getServiceFromStaticMethodService.php',
'tagged_iterator' => __DIR__.'/getTaggedIteratorService.php',
'tagged_iterator_foo' => __DIR__.'/getTaggedIteratorFooService.php',
);
$this->privates = array(
'factory_simple' => true,
'tagged_iterator_foo' => true,
);
$this->aliases = array(
'alias_for_alias' => 'foo',

View File

@ -49,9 +49,12 @@ class ProjectServiceContainer extends Container
'method_call1' => 'getMethodCall1Service',
'new_factory_service' => 'getNewFactoryServiceService',
'service_from_static_method' => 'getServiceFromStaticMethodService',
'tagged_iterator' => 'getTaggedIteratorService',
'tagged_iterator_foo' => 'getTaggedIteratorFooService',
);
$this->privates = array(
'factory_simple' => true,
'tagged_iterator_foo' => true,
);
$this->aliases = array(
'alias_for_alias' => 'foo',
@ -339,6 +342,19 @@ class ProjectServiceContainer extends Container
return $this->services['service_from_static_method'] = \Bar\FooClass::getInstance();
}
/**
* Gets the public 'tagged_iterator' shared service.
*
* @return \Bar
*/
protected function getTaggedIteratorService()
{
return $this->services['tagged_iterator'] = new \Bar(new RewindableGenerator(function () {
yield 0 => ${($_ = isset($this->services['foo']) ? $this->services['foo'] : $this->getFooService()) && false ?: '_'};
yield 1 => ${($_ = isset($this->services['tagged_iterator_foo']) ? $this->services['tagged_iterator_foo'] : $this->services['tagged_iterator_foo'] = new \Bar()) && false ?: '_'};
}, 2));
}
/**
* Gets the private 'factory_simple' shared service.
*
@ -353,6 +369,16 @@ class ProjectServiceContainer extends Container
return $this->services['factory_simple'] = new \SimpleFactoryClass('foo');
}
/**
* Gets the private 'tagged_iterator_foo' shared service.
*
* @return \Bar
*/
protected function getTaggedIteratorFooService()
{
return $this->services['tagged_iterator_foo'] = new \Bar();
}
/**
* {@inheritdoc}
*/

View File

@ -133,6 +133,12 @@
</argument>
<argument type="iterator"/>
</service>
<service id="tagged_iterator_foo" class="Bar" public="false">
<tag name="foo"/>
</service>
<service id="tagged_iterator" class="Bar" public="true">
<argument type="tagged" tag="foo"/>
</service>
<service id="Psr\Container\ContainerInterface" alias="service_container" public="false"/>
<service id="Symfony\Component\DependencyInjection\ContainerInterface" alias="service_container" public="false"/>
<service id="alias_for_foo" alias="foo" public="true"/>

View File

@ -145,6 +145,16 @@ services:
alias_for_alias:
alias: 'foo'
public: true
tagged_iterator_foo:
class: Bar
tags:
- { name: foo }
public: false
tagged_iterator:
class: Bar
arguments:
- !tagged foo
public: true
Psr\Container\ContainerInterface:
alias: service_container
public: false

View File

@ -774,8 +774,8 @@ class Inline
$nextOffset += strspn($value, ' ', $nextOffset);
// Is followed by a scalar
if (!isset($value[$nextOffset]) || !in_array($value[$nextOffset], array('[', '{'), true)) {
// Manage scalars in {@link self::evaluateScalar()}
if ((!isset($value[$nextOffset]) || !in_array($value[$nextOffset], array('[', '{'), true)) && 'tagged' !== $tag) {
// Manage non-whitelisted scalars in {@link self::evaluateScalar()}
return;
}

View File

@ -698,6 +698,8 @@ class Parser
if ('' !== $matches['tag']) {
if ('!!binary' === $matches['tag']) {
return Inline::evaluateBinaryScalar($data);
} elseif ('tagged' === $matches['tag']) {
return new TaggedValue(substr($matches['tag'], 1), $data);
} elseif ('!' !== $matches['tag']) {
@trigger_error($this->getDeprecationMessage(sprintf('Using the custom tag "%s" for the value "%s" is deprecated since version 3.3. It will be replaced by an instance of %s in 4.0.', $matches['tag'], $data, TaggedValue::class)), E_USER_DEPRECATED);
}