8000 [DI] add syntax to stack decorators · symfony/symfony@98eeeae · GitHub
[go: up one dir, main page]

Skip to content

Commit 98eeeae

Browse files
[DI] add syntax to stack decorators
1 parent b2f210f commit 98eeeae

File tree

14 files changed

+559
-62
lines changed

14 files changed

+559
-62
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class UnusedTagsPass implements CompilerPassInterface
3838
'container.service_locator',
3939
'container.service_locator_context',
4040
'container.service_subscriber',
41+
'container.stack',
4142
'controller.argument_value_resolver',
4243
'controller.service_arguments',
4344
'data_collector',

src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public function __construct()
5151
$this->optimizationPasses = [[
5252
new AutoAliasServicePass(),
5353
new ValidateEnvPlaceholdersPass(),
54+
new ResolveDecoratorStackPass(),
5455
new ResolveChildDefinitionsPass(),
5556
new RegisterServiceSubscribersPass(),
5657
new ResolveParameterPlaceHoldersPass(false, false),
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Alias;
15+
use Symfony\Component\DependencyInjection\ChildDefinition;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\Definition;
18+
use Symfony\Comp 47CA onent\DependencyInjection\Exception\InvalidArgumentException;
19+
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
20+
use Symfony\Component\DependencyInjection\Reference;
21+
22+
/**
23+
* @author Nicolas Grekas <p@tchwork.com>
24+
*/
25+
class ResolveDecoratorStackPass implements CompilerPassInterface
26+
{
27+
private $tag;
28+
29+
public function __construct(string $tag = 'container.stack')
30+
{
31+
$this->tag = $tag;
32+
}
33+
34+
public function process(ContainerBuilder $container)
35+
{
36+
$stacks = [];
37+
38+
foreach ($container->findTaggedServiceIds($this->tag) as $id => $tags) {
39+
$definition = $container->getDefinition($id);
40+
41+
if (!$definition instanceof ChildDefinition) {
42+
throw new InvalidArgumentException(sprintf('Invalid service "%s": only definitions with a "parent" can have the "%s" tag.', $id, $this->tag));
43+
}
44+
45+
if (!$stack = $definition->getArguments()) {
46+
throw new InvalidArgumentException(sprintf('Invalid service "%s": the stack of decorators is empty.', $id));
47+
}
48+
49+
$stacks[$id] = $stack;
50+
}
51+
52+
if (!$stacks) {
53+
return;
54+
}
55+
56+
$resolvedDefinitions = [];
57+
58+
foreach ($container->getDefinitions() as $id => $definition) {
59+
if (!isset($stacks[$id])) {
60+
$resolvedDefinitions[$id] = $definition;
61+
continue;
62+
}
63+
64+
foreach (array_reverse($this->resolveStack($stacks, [$id]), true) as $k => $v) {
65+
$resolvedDefinitions[$k] = $v;
66+
}
67+
68+
$alias = $container->setAlias($id, $k);
69+
70+
if ($definition->getChanges()['public'] ?? false) {
71+
$alias->setPublic($definition->isPublic());
72+
}
73+
74+
if ($definition->isDeprecated()) {
75+
$alias->setDeprecated(...array_values($definition->getDeprecation('%alias_id%')));
76+
}
77+
}
78+
79+
$container->setDefinitions($resolvedDefinitions);
80+
}
81+
82+
private function resolveStack(array $stacks, array $path): array
83+
{
84+
$definitions = [];
85+
$id = end($path);
86+
$prefix = '.'.$id.'.';
87+
88+
if (!isset($stacks[$id])) {
89+
return [$id => new ChildDefinition($id)];
90+
}
91+
92+
if (key($path) !== $searchKey = array_search($id, $path)) {
93+
throw new ServiceCircularReferenceException($id, \array_slice($path, $searchKey));
94+
}
95+
96+
foreach ($stacks[$id] as $k => $definition) {
97+
if ($definition instanceof ChildDefinition && isset($stacks[$definition->getParent()])) {
98+
$path[] = $definition->getParent();
99+
$definition = unserialize(serialize($definition)); // deep clone
100+
} elseif ($definition instanceof Definition) {
101+
$definitions[$decoratedId = $prefix.$k] = $definition;
102+
continue;
103+
} elseif ($definition instanceof Reference || $definition instanceof Alias) {
104+
$path[] = (string) $definition;
105+
} else {
106+
throw new InvalidArgumentException(sprintf('Invalid service "%s": unexpected value of type "%s" found in the stack of decorators.', $id, get_debug_type($definition)));
107+
}
108+
109+
$p = $prefix.$k;
110+
111+
foreach ($this->resolveStack($stacks, $path) as $k => $v) {
112+
$definitions[$decoratedId = $p.$k] = $definition instanceof ChildDefinition ? $definition->setParent($k) : new ChildDefinition($k);
113+
$definition = null;
114+
}
115+
array_pop($path);
116+
}
117+
118+
if (1 === \count($path)) {
119+
foreach ($definitions as $k => $definition) {
120+
$definition->setPublic(false)->setTags([])->setDecoratedService($decoratedId);
121+
}
122+
$definition->setDecoratedService(null);
123+
}
124+
125+
return $definitions;
126+
}
127+
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ final public function get(string $id): ServiceConfigurator
8181
return $this->parent->get($id);
8282
}
8383

84+
/**
85+
* Registers a stack of decorator services.
86+
*
87+
* @param InlineServiceConfigurator[]|ReferenceConfigurator[] $services
88+
*/
89+
final public function stack(string $id, array $services): AliasConfigurator
90+
{
91+
$this->__destruct();
92+
93+
return $this->parent->stack($id, $services);
94+
}
95+
8496
/**
8597
* Registers a service.
8698
*/

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\DependencyInjection\ChildDefinition;
1616
use Symfony\Component\DependencyInjection\ContainerBuilder;
1717
use Symfony\Component\DependencyInjection\Definition;
18+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1819
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
1920
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
2021

@@ -131,6 +132,39 @@ final public function get(string $id): ServiceConfigurator
131132
return new ServiceConfigurator($this->container, $definition->getInstanceofConditionals(), true, $this, $definition, $id, []);
132133
}
133134

135+
/**
136+
* Registers a stack of decorator services.
137+
*
138+
* @param InlineServiceConfigurator[]|ReferenceConfigurator[] $services
139+
*/
140+
final public function stack(string $id, array $services): AliasConfigurator
141+
{
142+
foreach ($services as $i => $service) {
143+
if ($service instanceof InlineServiceConfigurator) {
144+
$definition = $service->definition->setInstanceofConditionals($this->instanceof);
145+
146+
$changes = $definition->getChanges();
147+
$definition->setAutowired((isset($changes['autowired']) ? $definition : $this->defaults)->isAutowired());
148+
$definition->setAutoconfigured((isset($changes['autoconfigured']) ? $definition : $this->defaults)->isAutoconfigured());
149+
$definition->setBindings(array_merge($this->defaults->getBindings(), $definition->getBindings()));
150+
$definition->setChanges($changes);
151+
152+
$services[$i] = $definition;
153+
} elseif (!$service instanceof ReferenceConfigurator) {
154+
throw new InvalidArgumentException(sprintf('"%s()" expects a list of definitions as returned by "%s()" or "%s()", "%s" given at index "%s" for service "%s".', __METHOD__, InlineServiceConfigurator::FACTORY, ReferenceConfigurator::FACTORY, $service instanceof AbstractConfigurator ? $service::FACTORY.'()' : get_debug_type($service)), $i, $id);
155+
}
156+
}
157+
158+
$alias = $this->alias($id, '');
159+
$alias->definition = $this->set($id)
160+
->parent('')
161+
->args($services)
162+
->tag('container.stack')
163+
->definition;
164+
165+
return $alias;
166+
}
167+
134168
/**
135169
* Registers a service.
136170
*/

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

Lines changed: 43 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,12 @@ private function parseImports(\DOMDocument $xml, string $file)
112112
}
113113
}
114114

115-
private function parseDefinitions(\DOMDocument $xml, string $file, array $defaults)
115+
private function parseDefinitions(\DOMDocument $xml, string $file, Definition $defaults)
116116
{
117117
$xpath = new \DOMXPath($xml);
118118
$xpath->registerNamespace('container', self::NS);
119119

120-
if (false === $services = $xpath->query('//container:services/container:service|//container:services/container:prototype')) {
120+
if (false === $services = $xpath->query('//container:services/container:service|//container:services/container:prototype|//container:services/container:stack')) {
121121
return;
122122
}
123123
$this->setCurrentDir(\dirname($file));
@@ -126,12 +126,34 @@ private function parseDefinitions(\DOMDocument $xml, string $file, array $defaul
126126
$this->isLoadingInstanceof = true;
127127
$instanceof = $xpath->query('//container:services/container:instanceof');
128128
foreach ($instanceof as $service) {
129-
$this->setDefinition((string) $service->getAttribute('id'), $this->parseDefinition($service, $file, []));
129+
$this->setDefinition((string) $service->getAttribute('id'), $this->parseDefinition($service, $file, new Definition()));
130130
}
131131

132132
$this->isLoadingInstanceof = false;
133133
foreach ($services as $service) {
134-
if (null !== $definition = $this->parseDefinition($service, $file, $defaults)) {
134+
if ('stack' === $service->tagName) {
135+
$service->setAttribute('parent', '-');
136+
$definition = $this->parseDefinition($service, $file, $defaults)
137+
->setTags(array_merge_recursive(['container.stack' => [[]]], $defaults->getTags()))
138+
;
139+
$this->setDefinition($id = (string) $service->getAttribute('id'), $definition);
140+
$stack = [];
141+
142+
foreach ($this->getChildren($service, 'service') as $k => $frame) {
143+
$k = $frame->getAttribute('id') ?: $k;
144+
$frame->setAttribute('id', $id.'" at index "'.$k);
145+
146+
if ($alias = $frame->getAttribute('alias')) {
147+
$this->validateAlias($frame, $file);
148+
$stack[$k] = new Reference($alias);
149+
} else {
150+
$stack[$k] = $this->parseDefinition($frame, $file, $defaults)
151+
->setInstanceofConditionals($this->instanceof);
152+
}
153+
}
154+
155+
$definition->setArguments($stack);
156+
} elseif (null !== $definition = $this->parseDefinition($service, $file, $defaults)) {
135157
if ('prototype' === $service->tagName) {
136158
$excludes = array_column($this->getChildren($service, 'exclude'), 'nodeValue');
137159
if ($service->hasAttribute('exclude')) {
@@ -148,60 +170,33 @@ private function parseDefinitions(\DOMDocument $xml, string $file, array $defaul
148170
}
149171
}
150172

151-
/**
152-
* Get service defaults.
153-
*/
154-
private function getServiceDefaults(\DOMDocument $xml, string $file): array
173+
private function getServiceDefaults(\DOMDocument $xml, string $file): Definition
155174
{
156175
$xpath = new \DOMXPath($xml);
157176
$xpath->registerNamespace('container', self::NS);
158177

159178
if (null === $defaultsNode = $xpath->query('//container:services/container:defaults')->item(0)) {
160-
return [];
161-
}
162-
163-
$bindings = [];
164-
foreach ($this->getArgumentsAsPhp($defaultsNode, 'bind', $file) as $argument => $value) {
165-
$bindings[$argument] = new BoundArgument($value, true, BoundArgument::DEFAULTS_BINDING, $file);
179+
return new Definition();
166180
}
167181

168-
$defaults = [
169-
'tags' => $this->getChildren($defaultsNode, 'tag'),
170-
'bind' => $bindings,
171-
];
172-
173-
foreach ($defaults['tags'] as $tag) {
174-
if ('' === $tag->getAttribute('name')) {
175-
throw new InvalidArgumentException(sprintf('The tag name for tag "<defaults>" in "%s" must be a non-empty string.', $file));
176-
}
177-
}
182+
$defaultsNode->setAttribute('id', '<defaults>');
178183

179-
if ($defaultsNode->hasAttribute('autowire')) {
180-
$defaults['autowire'] = XmlUtils::phpize($defaultsNode->getAttribute('autowire'));
181-
}
182-
if ($defaultsNode->hasAttribute('public')) {
183-
$defaults['public'] = XmlUtils::phpize($defaultsNode->getAttribute('public'));
184-
}
185-
if ($defaultsNode->hasAttribute('autoconfigure')) {
186-
$defaults['autoconfigure'] = XmlUtils::phpize($defaultsNode->getAttribute('autoconfigure'));
187-
}
188-
189-
return $defaults;
184+
return $this->parseDefinition($defaultsNode, $file, new Definition());
190185
}
191186

192187
/**
193188
* Parses an individual Definition.
194189
*/
195-
private function parseDefinition(\DOMElement $service, string $file, array $defaults): ?Definition
190+
private function parseDefinition(\DOMElement $service, string $file, Definition $defaults): ?Definition
196191
{
197192
if ($alias = $service->getAttribute('alias')) {
198193
$this->validateAlias($service, $file);
199194

200195
$this->container->setAlias((string) $service->getAttribute('id'), $alias = new Alias($alias));
201196
if ($publicAttr = $service->getAttribute< 10000 /span>('public')) {
202197
$alias->setPublic(XmlUtils::phpize($publicAttr));
203-
} elseif (isset($defaults['public'])) {
204-
$alias->setPublic($defaults['public']);
198+
} elseif ($defaults->getChanges()['public'] ?? false) {
199+
$alias->setPublic($defaults->isPublic());
205200
}
206201

207202
if ($deprecated = $this->getChildren($service, 'deprecated')) {
@@ -231,16 +226,11 @@ private function parseDefinition(\DOMElement $service, string $file, array $defa
231226
$definition = new Definition();
232227
}
233228

234-
if (isset($defaults['public'])) {
235-
$definition->setPublic($defaults['public']);
229+
if ($defaults->getChanges()['public'] ?? false) {
230+
$definition->setPublic($defaults->isPublic());
236231
}
237-
if (isset($defaults['autowire'])) {
238-
$definition->setAutowired($defaults['autowire']);
239-
}
240-
if (isset($defaults['autoconfigure'])) {
241-
$definition->setAutoconfigured($defaults['autoconfigure']);
242-
}
243-
232+
$definition->setAutowired($defaults->isAutowired());
233+
$definition->setAutoconfigured($defaults->isAutoconfigured());
244234
$definition->setChanges([]);
245235

246236
foreach (['class', 'public', 'shared', 'synthetic', 'abstract'] as $key) {
@@ -324,10 +314,6 @@ private function parseDefinition(\DOMElement $service, string $file, array $defa
324314

325315
$tags = $this->getChildren($service, 'tag');
326316

327-
if (!empty($defaults['tags'])) {
328-
$tags = array_merge($tags, $defaults['tags']);
329-
}
330-
331317
foreach ($tags as $tag) {
332318
$parameters = [];
333319
foreach ($tag->attributes as $name => $node) {
@@ -349,16 +335,17 @@ private function parseDefinition(\DOMElement $service, string $file, array $defa
349335
$definition->addTag($tag->getAttribute('name'), $parameters);
350336
}
351337

338+
$definition->setTags(array_merge_recursive($definition->getTags(), $defaults->getTags()));
339+
352340
$bindings = $this->getArgumentsAsPhp($service, 'bind', $file);
353341
$bindingType = $this->isLoadingInstanceof ? BoundArgument::INSTANCEOF_BINDING : BoundArgument::SERVICE_BINDING;
354342
foreach ($bindings as $argument => $value) {
355343
$bindings[$argument] = new BoundArgument($value, true, $bindingType, $file);
356344
}
357345

358-
if (isset($defaults['bind'])) {
359-
// deep clone, to avoid multiple process of the same instance in the passes
360-
$bindings = array_merge(unserialize(serialize($defaults['bind'])), $bindings);
361-
}
346+
// deep clone, to avoid multiple process of the same instance in the passes
347+
$bindings = array_merge(unserialize(serialize($defaults->getBindings())), $bindings);
348+
362349
if ($bindings) {
363350
$definition->setBindings($bindings);
364351
}
@@ -443,7 +430,7 @@ private function processAnonymousServices(\DOMDocument $xml, string $file)
443430
// resolve definitions
444431
uksort($definitions, 'strnatcmp');
445432
foreach (array_reverse($definitions) as $id => list($domElement, $file)) {
446-
if (null !== $definition = $this->parseDefinition($domElement, $file, [])) {
433+
if (null !== $definition = $this->parseDefinition($domElement, $file, new Definition())) {
447434
$this->setDefinition($id, $definition);
448435
}
449436
}

0 commit comments

Comments
 (0)
0