Skip to content

Commit d706f59

Browse files
feature #30348 [DependencyInjection] Add ability to define an index for service in an injected service locator argument (XuruDragon, nicolas-grekas)
This PR was merged into the 4.3-dev branch. Discussion ---------- [DependencyInjection] Add ability to define an index for service in an injected service locator argument | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | n/a | License | MIT | Doc PR | in progress / symfony/symfony-docs#... It's more or less the same thing then the PR #30257 but for a service locator argument Add a simple way to specify an index based on a tag attribute to simplify retrieving a specific service when injecting a service locator as argument into services, but also a way to fallback to a static method on the service class. Yaml: ```yaml services: foo_service: class: Foo tags: - {name: foo_tag, key: foo_service} foo_service_tagged: class: Bar arguments: - !tagged_locator tag: 'foo_tag' index_by: 'key' default_index_method: 'static_method' ``` XML: ```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" key="foo_service" /> </service> <service id="foo_tagged_iterator" class="Bar" public="true"> <argument type="tagged_locator" tag="foo_tag" index-by="key" default-index-method="static_method" /> </service> </services> </container> ``` PHP: ```php // config/services.php use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; $container->register(Foo::class) ->addTag('foo_tag', ['key' => 'foo_service']); $container->register(App\Handler\HandlerCollection::class) // inject all services tagged with app.handler as first argument ->addArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('app.handler', 'key'))); ``` Usage: ```php // src/Handler/HandlerCollection.php namespace App\Handler; use Symfony\Component\DependencyInjection\ServiceLocator; class HandlerCollection { public function __construct(ServiceLocator $serviceLocator) { $foo = $serviceLocator->get('foo_service'): } } ``` 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 ------- cb3c56bc0c Support indexing tagged locators by FQCN as fallback 250a2c8332 [DI] Allow tagged_locator tag to be used as an argument
2 parents 7eb46b5 + 427531b commit d706f59

18 files changed

+251
-54
lines changed

Argument/ServiceLocatorArgument.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Argument;
1313

14+
use Symfony\Component\DependencyInjection\Reference;
15+
1416
/**
1517
* Represents a closure acting as a service locator.
1618
*
@@ -19,4 +21,24 @@
1921
class ServiceLocatorArgument implements ArgumentInterface
2022
{
2123
use ReferenceSetArgumentTrait;
24+
25+
private $taggedIteratorArgument;
26+
27+
/**
28+
* @param Reference[]|TaggedIteratorArgument $values
29+
*/
30+
public function __construct($values = [])
31+
{
32+
if ($values instanceof TaggedIteratorArgument) {
33+
$this->taggedIteratorArgument = $values;
34+
$this->values = [];
35+
} else {
36+
$this->setValues($values);
37+
}
38+
}
39+
40+
public function getTaggedIteratorArgument(): ?TaggedIteratorArgument
41+
{
42+
return $this->taggedIteratorArgument;
43+
}
2244
}

Argument/TaggedIteratorArgument.php

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,22 @@ class TaggedIteratorArgument extends IteratorArgument
2121
private $tag;
2222
private $indexAttribute;
2323
private $defaultIndexMethod;
24+
private $useFqcnAsFallback = false;
2425

2526
/**
26-
* TaggedIteratorArgument constructor.
27-
*
2827
* @param string $tag The name of the tag identifying the target services
2928
* @param string|null $indexAttribute The name of the attribute that defines the key referencing each service in the tagged collection
3029
* @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
30+
* @param bool $useFqcnAsFallback Whether the FQCN of the service should be used as index when neither the attribute nor the method are defined
3131
*/
32-
public function __construct(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null)
32+
public function __construct(string $tag, string $indexAttribute = null, string $defaultIndexMethod = null, bool $useFqcnAsFallback = false)
3333
{
3434
parent::__construct([]);
3535

3636
$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-
}
37+
$this->indexAttribute = $indexAttribute;
38+
$this->defaultIndexMethod = $defaultIndexMethod ?: ('getDefault'.str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $indexAttribute ?? ''))).'Name');
39+
$this->useFqcnAsFallback = $useFqcnAsFallback;
4240
}
4341

4442
public function getTag()
@@ -55,4 +53,9 @@ public function getDefaultIndexMethod(): ?string
5553
{
5654
return $this->defaultIndexMethod;
5755
}
56+
57+
public function useFqcnAsFallback(): bool
58+
{
59+
return $this->useFqcnAsFallback;
60+
}
5861
}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CHANGELOG
1010
* made `ContainerParametersResource` final and not implement `Serializable` anymore
1111
* added `ReverseContainer`: a container that turns services back to their ids
1212
* added ability to define an index for a tagged collection
13+
* added ability to define an index for services in an injected service locator argument
1314

1415
4.2.0
1516
-----

Compiler/PriorityTaggedServiceTrait.php

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\DependencyInjection\ContainerBuilder;
1616
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1717
use Symfony\Component\DependencyInjection\Reference;
18+
use Symfony\Component\DependencyInjection\TypedReference;
1819

1920
/**
2021
* Trait that allows a generic method to find and sort service by priority option in the tag.
@@ -40,34 +41,48 @@ trait PriorityTaggedServiceTrait
4041
*/
4142
private function findAndSortTaggedServices($tagName, ContainerBuilder $container)
4243
{
43-
$indexAttribute = $defaultIndexMethod = null;
44+
$indexAttribute = $defaultIndexMethod = $useFqcnAsFallback = null;
45+
4446
if ($tagName instanceof TaggedIteratorArgument) {
4547
$indexAttribute = $tagName->getIndexAttribute();
4648
$defaultIndexMethod = $tagName->getDefaultIndexMethod();
49+
$useFqcnAsFallback = $tagName->useFqcnAsFallback();
4750
$tagName = $tagName->getTag();
4851
}
52+
4953
$services = [];
5054

5155
foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $attributes) {
5256
$priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
5357

54-
if (null === $indexAttribute) {
58+
if (null === $indexAttribute && !$useFqcnAsFallback) {
5559
$services[$priority][] = new Reference($serviceId);
5660

5761
continue;
5862
}
5963

60-
if (isset($attributes[0][$indexAttribute])) {
61-
$services[$priority][$attributes[0][$indexAttribute]] = new Reference($serviceId);
64+
$class = $container->getDefinition($serviceId)->getClass();
65+
$class = $container->getParameterBag()->resolveValue($class) ?: null;
66+
67+
if (null !== $indexAttribute && isset($attributes[0][$indexAttribute])) {
68+
$services[$priority][$attributes[0][$indexAttribute]] = new TypedReference($serviceId, $class, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $attributes[0][$indexAttribute]);
6269

6370
continue;
6471
}
6572

66-
if (!$r = $container->getReflectionClass($class = $container->getDefinition($serviceId)->getClass())) {
73+
if (!$r = $container->getReflectionClass($class)) {
6774
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $serviceId));
6875
}
6976

77+
$class = $r->name;
78+
7079
if (!$r->hasMethod($defaultIndexMethod)) {
80+
if ($useFqcnAsFallback) {
81+
$services[$priority][$class] = new TypedReference($serviceId, $class);
82+
83+
continue;
84+
}
85+
7186
throw new InvalidArgumentException(sprintf('Method "%s::%s()" not found: tag "%s" on service "%s" is missing "%s" attribute.', $class, $defaultIndexMethod, $tagName, $serviceId, $indexAttribute));
7287
}
7388

@@ -85,7 +100,7 @@ private function findAndSortTaggedServices($tagName, ContainerBuilder $container
85100
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));
86101
}
87102

88-
$services[$priority][$key] = new Reference($serviceId);
103+
$services[$priority][$key] = new TypedReference($serviceId, $class, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $key);
89104
}
90105

91106
if ($services) {

Compiler/ServiceLocatorTagPass.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,18 @@
2727
*/
2828
final class ServiceLocatorTagPass extends AbstractRecursivePass
2929
{
30+
use PriorityTaggedServiceTrait;
31+
3032
protected function processValue($value, $isRoot = false)
3133
{
3234
if ($value instanceof ServiceLocatorArgument) {
35+
if ($value->getTaggedIteratorArgument()) {
36+
$value->setValues($this->findAndSortTaggedServices($value->getTaggedIteratorArgument(), $this->container));
37+
}
38+
3339
return self::register($this->container, $value->getValues());
3440
}
41+
3542
if (!$value instanceof Definition || !$value->hasTag('container.service_locator')) {
3643
return parent::processValue($value, $isRoot);
3744
}

Dumper/XmlDumper.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -280,19 +280,19 @@ private function convertParameters(array $parameters, $type, \DOMElement $parent
280280
if ($value instanceof ServiceClosureArgument) {
281281
$value = $value->getValues()[0];
282282
}
283-
if (\is_array($value)) {
283+
if (\is_array($tag = $value)) {
284284
$element->setAttribute('type', 'collection');
285285
$this->convertParameters($value, $type, $element, 'key');
286-
} elseif ($value instanceof TaggedIteratorArgument) {
287-
$element->setAttribute('type', 'tagged');
288-
$element->setAttribute('tag', $value->getTag());
286+
} elseif ($value instanceof TaggedIteratorArgument || ($value instanceof ServiceLocatorArgument && $tag = $value->getTaggedIteratorArgument())) {
287+
$element->setAttribute('type', $value instanceof TaggedIteratorArgument ? 'tagged' : 'tagged_locator');
288+
$element->setAttribute('tag', $tag->getTag());
289289

290-
if (null !== $value->getIndexAttribute()) {
291-
$element->setAttribute('index-by', $value->getIndexAttribute());
292-
}
290+
if (null !== $tag->getIndexAttribute()) {
291+
$element->setAttribute('index-by', $tag->getIndexAttribute());
293292

294-
if (null !== $value->getDefaultIndexMethod()) {
295-
$element->setAttribute('default-index-method', $value->getDefaultIndexMethod());
293+
if (null !== $tag->getDefaultIndexMethod()) {
294+
$element->setAttribute('default-index-method', $tag->getDefaultIndexMethod());
295+
}
296296
}
297297
} elseif ($value instanceof IteratorArgument) {
298298
$element->setAttribute('type', 'iterator');

Dumper/YamlDumper.php

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -232,22 +232,25 @@ private function dumpValue($value)
232232
$value = $value->getValues()[0];
233233
}
234234
if ($value instanceof ArgumentInterface) {
235-
if ($value instanceof TaggedIteratorArgument) {
236-
if (null !== $value->getIndexAttribute()) {
237-
$taggedValueContent = [
238-
'tag' => $value->getTag(),
239-
'index_by' => $value->getIndexAttribute(),
235+
$tag = $value;
236+
237+
if ($value instanceof TaggedIteratorArgument || ($value instanceof ServiceLocatorArgument && $tag = $value->getTaggedIteratorArgument())) {
238+
if (null === $tag->getIndexAttribute()) {
239+
$content = $tag->getTag();
240+
} else {
241+
$content = [
242+
'tag' => $tag->getTag(),
243+
'index_by' => $tag->getIndexAttribute(),
240244
];
241245

242-
if (null !== $value->getDefaultIndexMethod()) {
243-
$taggedValueContent['default_index_method'] = $value->getDefaultIndexMethod();
246+
if (null !== $tag->getDefaultIndexMethod()) {
247+
$content['default_index_method'] = $tag->getDefaultIndexMethod();
244248
}
245-
246-
return new TaggedValue('tagged', $taggedValueContent);
247249
}
248250

249-
return new TaggedValue('tagged', $value->getTag());
251+
return new TaggedValue($value instanceof TaggedIteratorArgument ? 'tagged' : 'tagged_locator', $content);
250252
}
253+
251254
if ($value instanceof IteratorArgument) {
252255
$tag = 'iterator';
253256
} elseif ($value instanceof ServiceLocatorArgument) {

Loader/Configurator/ContainerConfigurator.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,14 @@ function tagged(string $tag, string $indexAttribute = null, string $defaultIndex
121121
return new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod);
122122
}
123123

124+
/**
125+
* Creates a service locator by tag name.
126+
*/
127+
function tagged_locator(string $tag, string $indexAttribute, string $defaultIndexMethod = null): ServiceLocatorArgument
128+
{
129+
return new ServiceLocatorArgument(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, true));
130+
}
131+
124132
/**
125133
* Creates an expression.
126134
*/

Loader/XmlFileLoader.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -534,11 +534,19 @@ private function getArgumentsAsPhp(\DOMElement $node, $name, $file, $lowercase =
534534
}
535535
break;
536536
case 'tagged':
537+
case 'tagged_locator':
538+
$type = $arg->getAttribute('type');
539+
$forLocator = 'tagged_locator' === $type;
540+
537541
if (!$arg->getAttribute('tag')) {
538-
throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="tagged" has no or empty "tag" attribute in "%s".', $name, $file));
542+
throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="%s" has no or empty "tag" attribute in "%s".', $name, $type, $file));
539543
}
540544

541-
$arguments[$key] = new TaggedIteratorArgument($arg->getAttribute('tag'), $arg->getAttribute('index-by') ?: null, $arg->getAttribute('default-index-method') ?: null);
545+
$arguments[$key] = new TaggedIteratorArgument($arg->getAttribute('tag'), $arg->getAttribute('index-by') ?: null, $arg->getAttribute('default-index-method') ?: null, $forLocator);
546+
547+
if ($forLocator) {
548+
$arguments[$key] = new ServiceLocatorArgument($arguments[$key]);
549+
}
542550
break;
543551
case 'binary':
544552
if (false === $value = base64_decode($arg->nodeValue)) {

Loader/YamlFileLoader.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -702,27 +702,37 @@ private function resolveServices($value, $file, $isParameter = false)
702702
if (!\is_array($argument)) {
703703
throw new InvalidArgumentException(sprintf('"!service_locator" tag only accepts maps in "%s".', $file));
704704
}
705+
705706
$argument = $this->resolveServices($argument, $file, $isParameter);
707+
706708
try {
707709
return new ServiceLocatorArgument($argument);
708710
} catch (InvalidArgumentException $e) {
709711
throw new InvalidArgumentException(sprintf('"!service_locator" tag only accepts maps of "@service" references in "%s".', $file));
710712
}
711713
}
712-
if ('tagged' === $value->getTag()) {
714+
if (\in_array($value->getTag(), ['tagged', 'tagged_locator'], true)) {
715+
$forLocator = 'tagged_locator' === $value->getTag();
716+
713717
if (\is_string($argument) && $argument) {
714-
return new TaggedIteratorArgument($argument);
718+
return new TaggedIteratorArgument($argument, null, null, $forLocator);
715719
}
716720

717721
if (\is_array($argument) && isset($argument['tag']) && $argument['tag']) {
718722
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)));
723+
throw new InvalidArgumentException(sprintf('"!%s" tag contains unsupported key "%s"; supported ones are "tag", "index_by" and "default_index_method".', $value->getTag(), implode('"", "', $diff)));
724+
}
725+
726+
$argument = new TaggedIteratorArgument($argument['tag'], $argument['index_by'], $argument['default_index_method'] ?? null, $forLocator);
727+
728+
if ($forLocator) {
729+
$argument = new ServiceLocatorArgument($argument);
720730
}
721731

722-
return new TaggedIteratorArgument($argument['tag'], $argument['index_by'] ?? null, $argument['default_index_method'] ?? null);
732+
return $argument;
723733
}
724734

725-
throw new InvalidArgumentException(sprintf('"!tagged" tags only accept a non empty string or an array with a key "tag" in "%s".', $file));
735+
throw new InvalidArgumentException(sprintf('"!%s" tags only accept a non empty string or an array with a key "tag" in "%s".', $value->getTag(), $file));
726736
}
727737
if ('service' === $value->getTag()) {
728738
if ($isParameter) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@
264264
<xsd:enumeration value="iterator" />
265265
<xsd:enumeration value="service_locator" />
266266
<xsd:enumeration value="tagged" />
267+
<xsd:enumeration value="tagged_locator" />
267268
</xsd:restriction>
268269
</xsd:simpleType>
269270

0 commit comments

Comments
 (0)