[DI] Reference tagged services in config

This commit is contained in:
Roland Franssen 2017-03-28 12:05:21 +02:00 committed by Nicolas Grekas
parent d7885ec3b2
commit 979e58f370
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(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);
}