8000 feature #47364 [DependencyInjection] Allow array attributes for servi… · symfony/symfony@5ad5b8d · GitHub
[go: up one dir, main page]

Skip to content

Commit 5ad5b8d

Browse files
committed
feature #47364 [DependencyInjection] Allow array attributes for service tags (aschempp)
This PR was merged into the 6.2 branch. Discussion ---------- [DependencyInjection] Allow array attributes for service tags | Q | A | ------------- | --- | Branch? | 6.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix #38339, #38540 | License | MIT | Doc PR | symfony/symfony-docs#... <!-- required for new features --> This adds array attribute support for container service tags as described in #38339. The most notable change is the additions to the XML schema for services, because that's the current limitation for not using array. This is reopening #38540 as new PR since the other one was ("accidentially") closed 😊 /cc `@nicolas`-grekas Commits ------- edd8d77 [DependencyInjection] Allow array attributes for service tags
2 parents 88191f5 + edd8d77 commit 5ad5b8d

File tree

14 files changed

+181
-45
lines changed

14 files changed

+181
-45
lines changed

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,14 @@ private function addService(Definition $definition, ?string $id, \DOMElement $pa
137137
} else {
138138
$tag->appendChild($this->document->createTextNode($name));
139139
}
140-
foreach ($attributes as $key => $value) {
141-
$tag->setAttribute($key, $value ?? '');
140+
141+
// Check if we have recursive attributes
142+
if (array_filter($attributes, \is_array(...))) {
143+
$this->addTagRecursiveAttributes($tag, $attributes);
144+
} else {
145+
foreach ($attributes as $key => $value) {
146+
$tag->setAttribute($key, $value ?? '');
147+
}
142148
}
143149
$service->appendChild($tag);
144150
}
@@ -261,6 +267,22 @@ private function addServices(\DOMElement $parent)
261267
$parent->appendChild($services);
262268
}
263269

270+
private function addTagRecursiveAttributes(\DOMElement $parent, array $attributes)
271+
{
272+
foreach ($attributes as $name => $value) {
273+
$attribute = $this->document->createElement('attribute');
274+
$attribute->setAttribute('name', $name);
275+
276+
if (\is_array($value)) {
277+
$this->addTagRecursiveAttributes($attribute, $value);
278+
} else {
279+
$attribute->appendChild($this->document->createTextNode($value));
280+
}
281+
282+
$parent->appendChild($attribute);
283+
}
284+
}
285+
264286
private function convertParameters(array $parameters, string $type, \DOMElement $parent, string $keyAttribute = 'key')
265287
{
266288
$withKeys = !array_is_list($parameters);

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,7 @@ final public function tag(string $name, array $attributes = []): static
4848
throw new InvalidArgumentException('The tag name in "_defaults" must be a non-empty string.');
4949
}
5050

51-
foreach ($attributes as $attribute => $value) {
52-
if (null !== $value && !\is_scalar($value)) {
53-
throw new InvalidArgumentException(sprintf('Tag "%s", attribute "%s" in "_defaults" must be of a scalar-type.', $name, $attribute));
54-
}
55-
}
51+
$this->validateAttributes($name, $attributes);
5652

5753
$this->definition->addTag($name, $attributes);
5854

@@ -66,4 +62,15 @@ final public function instanceof(string $fqcn): InstanceofConfigurator
6662
{
6763
return $this->parent->instanceof($fqcn);
6864
}
65+
66+
private function validateAttributes(string $tagName, array $attributes, string $prefix = ''): void
67+
{
68+
foreach ($attributes as $attribute => $value) {
69+
if (\is_array($value)) {
70+
$this->validateAttributes($tagName, $value, $attribute.'.');
71+
} elseif (!\is_scalar($value ?? '')) {
72+
throw new InvalidArgumentException(sprintf('Tag "%s", attribute "%s" in "_defaults" must be of a scalar-type or an array of scalar-type.', $tagName, $prefix.$attribute));
73+
}
74+
}
75+
}
6976
}

src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/TagTrait.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,21 @@ final public function tag(string $name, array $attributes = []): static
2626
throw new InvalidArgumentException(sprintf('The tag name for service "%s" must be a non-empty string.', $this->id));
2727
}
2828

29-
foreach ($attributes as $attribute => $value) {
30-
if (!\is_scalar($value) && null !== $value) {
31-
throw new InvalidArgumentException(sprintf('A tag attribute must be of a scalar-type for service "%s", tag "%s", attribute "%s".', $this->id, $name, $attribute));
32-
}
33-
}
29+
$this->validateAttributes($name, $attributes);
3430

3531
$this->definition->addTag($name, $attributes);
3632

3733
return $this;
3834
}
35+
36+
private function validateAttributes(string $tagName, array $attributes, string $prefix = ''): void
37+
{
38+
foreach ($attributes as $attribute => $value) {
39+
if (\is_array($value)) {
40+
$this->validateAttributes($tagName, $value, $attribute.'.');
41+
} elseif (!\is_scalar($value ?? '')) {
42+
throw new InvalidArgumentException(sprintf('A tag attribute must be of a scalar-type or an array of scalar-types for service "%s", tag "%s", attribute "%s".', $this->id, $tagName, $prefix.$attribute));
43+
}
44+
}
45+
}
3946
}

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

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -336,10 +336,13 @@ private function parseDefinition(\DOMElement $service, string $file, Definition
336336
$tags = $this->getChildren($service, 'tag');
337337

338338
foreach ($tags as $tag) {
339-
$parameters = [];
340-
$tagName = $tag->nodeValue;
339+
if ('' === $tagName = $tag->hasChildNodes() || '' === $tag->nodeValue ? $tag->getAttribute('name') : $tag->nodeValue) {
340+
throw new InvalidArgumentException(sprintf('The tag name for service "%s" in "%s" must be a non-empty string.', (string) $service->getAttribute('id'), $file));
341+
}
342+
343+
$parameters = $this->getTagAttributes($tag, sprintf('The attribute name of tag "%s" for service "%s" in %s must be a non-empty string.', $tagName, (string) $service->getAttribute('id'), $file));
341344
foreach ($tag->attributes as $name => $node) {
342-
if ('name' === $name && '' === $tagName) {
345+
if ('name' === $name) {
343346
continue;
344347
}
345348

@@ -350,10 +353,6 @@ private function parseDefinition(\DOMElement $service, string $file, Definition
350353
$parameters[$name] = XmlUtils::phpize($node->nodeValue);
351354
}
352355

353-
if ('' === $tagName && '' === $tagName = $tag->getAttribute('name')) {
354-
throw new InvalidArgumentException(sprintf('The tag name for service "%s" in "%s" must be a non-empty string.', $service->getAttribute('id'), $file));
355-
}
356-
357356
$definition->addTag($tagName, $parameters);
358357
}
359358

@@ -592,6 +591,30 @@ private function getChildren(\DOMNode $node, string $name): array
592591
return $children;
593592
}
594593

594+
private function getTagAttributes(\DOMNode $node, string $missingName): array
595+
{
596+
$parameters = [];
597+
$children = $this->getChildren($node, 'attribute');
598+
599+
foreach ($children as $childNode) {
600+
if ('' === $name = $childNode->getAttribute('name')) {
601+
throw new InvalidArgumentException($missingName);
602+
}
603+
604+
if ($this->getChildren($childNode, 'attribute')) {
605+
$parameters[$name] = $this->getTagAttributes($childNode, $missingName);
606+
} else {
607+
if (str_contains($name, '-') && !str_contains($name, '_') && !\array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) {
608+
$parameters[$normalizedName] = XmlUtils::phpize($childNode->nodeValue);
609+
}
610+
// keep not normalized key
611+
$parameters[$name] = XmlUtils::phpize($childNode->nodeValue);
612+
}
613+
}
614+
615+
return $parameters;
616+
}
617+
595618
/**
596619
* Validates a documents XML schema.
597620
*

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -297,11 +297,7 @@ private function parseDefaults(array &$content, string $file): array
297297
throw new InvalidArgumentException(sprintf('The tag name in "_defaults" must be a non-empty string in "%s".', $file));
298298
}
299299

300-
foreach ($tag as $attribute => $value) {
301-
if (!\is_scalar($value) && null !== $value) {
302-
throw new InvalidArgumentException(sprintf('Tag "%s", attribute "%s" in "_defaults" must be of a scalar-type in "%s". Check your YAML syntax.', $name, $attribute, $file));
303-
}
304-
}
300+
$this->validateAttributes(sprintf('Tag "%s", attribute "%s" in "_defaults" must be of a scalar-type in "%s". Check your YAML syntax.', $name, '%s', $file), $tag);
305301
}
306302
}
307303

@@ -605,11 +601,7 @@ private function parseDefinition(string $id, array|string|null $service, string
605601
throw new InvalidArgumentException(sprintf('The tag name for service "%s" in "%s" must be a non-empty string.', $id, $file));
606602
}
607603

608-
foreach ($tag as $attribute => $value) {
609-
if (!\is_scalar($value) && null !== $value) {
610-
throw new InvalidArgumentException(sprintf('A "tags" attribute must be of a scalar-type for service "%s", tag "%s", attribute "%s" in "%s". Check your YAML syntax.', $id, $name, $attribute, $file));
611-
}
612-
}
604+
$this->validateAttributes(sprintf('A "tags" attribute must be of a scalar-type for service "%s", tag "%s", attribute "%s" in "%s". Check your YAML syntax.', $id, $name, '%s', $file), $tag);
613605

614606
$definition->addTag($name, $tag);
615607
}
@@ -948,4 +940,15 @@ private function checkDefinition(string $id, array $definition, string $file)
948940
}
949941
}
950942
}
943+
944+
private function validateAttributes(string $message, array $attributes, string $prefix = ''): void
945+
{
946+
foreach ($attributes as $attribute => $value) {
947+
if (\is_array($value)) {
948+
$this->validateAttributes($message, $value, $attribute.'.');
949+
} elseif (!\is_scalar($value ?? '')) {
950+
throw new InvalidArgumentException(sprintf($message, $prefix.$attribute));
951+
}
952+
}
953+
}
951954
}

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -219,12 +219,11 @@
219219
<xsd:attribute name="public" type="boolean" />
220220
</xsd:complexType>
221221

222-
<xsd:complexType name="tag">
223-
<xsd:simpleContent>
224-
<xsd:extension base="xsd:string">
225-
<xsd:anyAttribute namespace="##any" processContents="lax" />
226-
</xsd:extension>
227-
</xsd:simpleContent>
222+
<xsd:complexType name="tag" mixed="true">
223+
<xsd:choice minOccurs="0">
224+
<xsd:element name="attribute" type="tag_attribute" maxOccurs="unbounded"/>
225+
</xsd:choice>
226+
<xsd:anyAttribute namespace="##any" processContents="lax" />
228227
</xsd:complexType>
229228

230229
<xsd:complexType name="deprecated">
@@ -236,6 +235,13 @@
236235
</xsd:simpleContent>
237236
</xsd:complexType>
238237

238+
<xsd:complexType name="tag_attribute" mixed="true">
239+
<xsd:choice minOccurs="0">
240+
<xsd:element name="attribute" type="tag_attribute" maxOccurs="unbounded" />
241+
</xsd:choice>
242+
<xsd:attribute name="name" type="xsd:string" use="required" />
243+
</xsd:complexType>
244+
239245
<xsd:complexType name="parameters">
240246
<xsd:choice minOccurs="1" maxOccurs="unbounded">
241247
<xsd:element name="parameter" type="parameter" />

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,4 +288,12 @@ public function testDumpServiceWithAbstractArgument()
288288
$dumper = new XmlDumper($container);
289289
$this->assertStringEqualsFile(self::$fixturesPath.'/xml/services_with_abstract_argument.xml', $dumper->dump());
290290
}
291+
292+
public function testDumpNonScalarTags()
293+
{
294+
$container = include self::$fixturesPath.'/containers/container_non_scalar_tags.php';
295+
$dumper = new XmlDumper($container);
296+
297+
$this->assertEquals(file_get_contents(self::$fixturesPath.'/xml/services_with_array_tags.xml'), $dumper->dump());
298+
}
291299
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ public function testDumpServiceWithAbstractArgument()
173173
$this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services_with_abstract_argument.yml', $dumper->dump());
174174
}
175175

176+
public function testDumpNonScalarTags()
177+
{
178+
$container = include self::$fixturesPath.'/containers/container_non_scalar_tags.php';
179+
$dumper = new YamlDumper($container);
180+
181+
$this->assertEquals(file_get_contents(self::$fixturesPath.'/yaml/services_with_array_tags.yml'), $dumper->dump());
182+
}
183+
176184
private function assertEqualYamlStructure(string $expected, string $yaml, string $message = '')
177185
{
178186
$parser = new Parser();
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
require_once __DIR__.'/../includes/classes.php';
4+
require_once __DIR__.'/../includes/foo.php';
5+
6+
use Bar\FooClass;
7+
use Symfony\Component\DependencyInjection\ContainerBuilder;
8+
9+
$container = new ContainerBuilder();
10+
$container
11+
->register('foo', FooClass::class)
12+
->addTag('foo_tag', [
13+
'foo' => 'bar',
14+
'bar' => [
15+
'foo' => 'bar',
16+
'bar' => 'foo'
17+
]])
18+
;
19+
20+
return $container;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<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 https://symfony.com/schema/dic/services/services-1.0.xsd">
3+
<services>
4+
<service id="service_container" class="Symfony\Component\DependencyInjection\ContainerInterface" public="true" synthetic="true"/>
5+
<service id="foo" class="Bar\FooClass">
6+
<tag name="foo_tag">
7+
<attribute name="foo">bar</attribute>
8+
<attribute name="bar">
9+
<attribute name="foo">bar</attribute>
10+
<attribute name="bar">foo</attribute>
11+
</attribute>
12+
</tag>
13+
</service>
14+
</services>
15+
</container>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
services:
3+
service_container:
4+
class: Symfony\Component\DependencyInjection\ContainerInterface
5+
public: true
6+
synthetic: true
7+
foo:
8+
class: Bar\FooClass
9+
tags:
10+
- foo_tag: { foo: bar, bar: { foo: bar, bar: foo } }

src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/badtag3.yml renamed to src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/tag_array_arguments.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ services:
22
foo_service:
33
class: FooClass
44
tags:
5-
# tag-attribute is not a scalar
5+
# tag-attribute is an array
66
- { name: foo, bar: { foo: foo, bar: bar } }

src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,15 @@ public function testParseServiceClosure()
426426
$this->assertEquals(new ServiceClosureArgument(new Reference('bar', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), $container->getDefinition('foo')->getArgument(0));
427427
}
428428

429+
public function testParseServiceTagsWithArrayAttributes()
430+
{
431+
$container = new ContainerBuilder();
432+
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
433+
$loader->load('services_with_array_tags.xml');
434+
435+
$this->assertEquals(['foo_tag' => [['foo' => 'bar', 'bar' => ['foo' => 'bar', 'bar' => 'foo']]]], $container->getDefinition('foo')->getTags());
436+
}
437+
429438
public function testParseTagsWithoutNameThrowsException()
430439
{
431440
$this->expectException(InvalidArgumentException::class);

src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -426,16 +426,14 @@ public function testNameOnlyTagsAreAllowedAsString()
426426
$this->assertCount(1, $container->getDefinition('foo_service')->getTag('foo'));
427427
}
428428

429-
public function testTagWithAttributeArrayThrowsException()
429+
public function testTagWithAttributeArray()
430430
{
431-
$loader = new YamlFileLoader(new ContainerBuilder(), new FileLocator(self::$fixturesPath.'/yaml'));
432-
try {
433-
$loader->load('badtag3.yml');
434-
$this->fail('->load() should throw an exception when a tag-attribute is not a scalar');
435-
} catch (\Exception $e) {
436-
$this->assertInstanceOf(InvalidArgumentException::class, $e, '->load() throws an InvalidArgumentException if a tag-attribute is not a scalar');
437-
$this->assertStringStartsWith('A "tags" attribute must be of a scalar-type for service "foo_service", tag "foo", attribute "bar"', $e->getMessage(), '->load() throws an InvalidArgumentException if a tag-attribute is not a scalar');
438-
}
431+
$container = new ContainerBuilder();
432+
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
433+
$loader->load('tag_array_arguments.yml');
434+
435+
$definition = $container->getDefinition('foo_service');
436+
$this->assertEquals(['foo' => [['bar' => ['foo' => 'foo', 'bar' => 'bar']]]], $definition->getTags());
439437
}
440438

441439
public function testLoadYamlOnlyWithKeys()

0 commit comments

Comments
 (0)
0