8000 [DependencyInjection] Allow array attributes for service tags · symfony/symfony@edd8d77 · GitHub
[go: up one dir, main page]

Skip to content

Commit edd8d77

Browse files
aschemppnicolas-grekas
authored andcommitted
[DependencyInjection] Allow array attributes for service tags
1 parent b1b77f3 commit edd8d77

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
@@ -342,10 +342,13 @@ private function parseDefinition(\DOMElement $service, string $file, Definition
342342
$tags = $this->getChildren($service, 'tag');
343343

344344
foreach ($tags as $tag) {
345 EED3 -
$parameters = [];
346-
$tagName = $tag->nodeValue;
345+
if ('' === $tagName = $tag->hasChildNodes() || '' === $tag->nodeValue ? $tag->getAttribute('name') : $tag->nodeValue) {
346+
throw new InvalidArgumentException(sprintf('The tag name for service "%s" in "%s" must be a non-empty string.', (string) $service->getAttribute('id'), $file));
347+
}
348+
349+
$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));
347350
foreach ($tag->attributes as $name => $node) {
348-
if ('name' === $name && '' === $tagName) {
351+
if ('name' === $name) {
349352
continue;
350353
}
351354

@@ -356,10 +359,6 @@ private function parseDefinition(\DOMElement $service, string $file, Definition
356359
$parameters[$name] = XmlUtils::phpize($node->nodeValue);
357360
}
358361

359-
if ('' === $tagName && '' === $tagName = $tag->getAttribute('name')) {
360-
throw new InvalidArgumentException(sprintf('The tag name for service "%s" in "%s" must be a non-empty string.', $service->getAttribute('id'), $file));
361-
}
362-
363362
$definition->addTag($tagName, $parameters);
364363
}
365364

@@ -590,6 +589,30 @@ private function getChildren(\DOMNode $node, string $name): array
590589
return $children;
591590
}
592591

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

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

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

306-
foreach ($tag as $attribute => $value) {
307-
if (!\is_scalar($value) && null !== $value) {
308-
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));
309-
}
310-
}
306+
$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);
311307
}
312308
}
313309

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

614-
foreach ($tag as $attribute => $value) {
615-
if (!\is_scalar($value) && null !== $value) {
616-
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));
617-
}
618-
}
610+
$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);
619611

620612
$definition->addTag($name, $tag);
621613
}
@@ -954,4 +946,15 @@ private function checkDefinition(string $id, array $definition, string $file)
954946
}
955947
}
956948
}
949+
950+
private function validateAttributes(string $message, array $attributes, string $prefix = ''): void
951+
{
952+
foreach ($attributes as $attribute => $value) {
953+
if (\is_array($value)) {
954+
$this->validateAttributes($message, $value, $attribute.'.');
955+
} elseif (!\is_scalar($value ?? '')) {
956+
throw new InvalidArgumentException(sprintf($message, $prefix.$attribute));
957+
}
958+
}
959+
}
957960
}

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
@@ -264,4 +264,12 @@ public function testDumpServiceWithAbstractArgument()
264264
$dumper = new XmlDumper($container);
265265
$this->assertStringEqualsFile(self::$fixturesPath.'/xml/services_with_abstract_argument.xml', $dumper->dump());
266266
}
267+
268+
public function testDumpNonScalarTags()
269+
{
270+
$container = include self::$fixturesPath.'/containers/container_non_scalar_tags.php';
271+
$dumper = new XmlDumper($container);
272+
273+
$this->assertEquals(file_get_contents(self::$fixturesPath.'/xml/services_with_array_tags.xml'), $dumper->dump());
274+
}
267275
}

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

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

164+
public function testDumpNonScalarTags()
165+
{
166+
$container = include self::$fixturesPath.'/containers/container_non_scalar_tags.php';
167+
$dumper = new YamlDumper($container);
168+
169+
$this->assertEquals(file_get_contents(self::$fixturesPath.'/yaml/services_with_array_tags.yml'), $dumper->dump());
170+
}
171+
164172
private function assertEqualYamlStructure(string $expected, string $yaml, string $message = '')
165173
{
166174
$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
@@ -418,6 +418,15 @@ public function testParseServiceClosure()
418418
$this->assertEquals(new ServiceClosureArgument(new Reference('bar', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), $container->getDefinition('foo')->getArgument(0));
419419
}
420420

421+
public function testParseServiceTagsWithArrayAttributes()
422+
{
423+
$container = new ContainerBuilder();
424+
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
425+
$loader->load('services_with_array_tags.xml');
426+
427+
$this->assertEquals(['foo_tag' => [['foo' => 'bar', 'bar' => ['foo' => 'bar', 'bar' => 'foo']]]], $container->getDefinition('foo')->getTags());
428+
}
429+
421430
public function testParseTagsWithoutNameThrowsException()
422431
{
423432
$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
@@ -418,16 +418,14 @@ public function testNameOnlyTagsAreAllowedAsString()
418418
$this->assertCount(1, $container->getDefinition('foo_service')->getTag('foo'));
419419
}
420420

421-
public function testTagWithAttributeArrayThrowsException()
421+
public function testTagWithAttributeArray()
422422
{
423-
$loader = new YamlFileLoader(new ContainerBuilder(), new FileLocator(self::$fixturesPath.'/yaml'));
424-
try {
425-
$loader->load('badtag3.yml');
426-
$this->fail('->load() should throw an exception when a tag-attribute is not a scalar');
427-
} catch (\Exception $e) {
428-
$this->assertInstanceOf(InvalidArgumentException::class, $e, '->load() throws an InvalidArgumentException if a tag-attribute is not a scalar');
429-
$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');
430-
}
423+
$container = new ContainerBuilder();
424+
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
425+
$loader->load('tag_array_arguments.yml');
426+
427+
$definition = $container->getDefinition('foo_service');
428+
$this->assertEquals(['foo' => [['bar' => ['foo' => 'foo', 'bar' => 'bar']]]], $definition->getTags());
431429
}
432430

433431
public function testLoadYamlOnlyWithKeys()

0 commit comments

Comments
 (0)
0