Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 9e12b40

Browse filesBrowse files
[DI] add syntax to stack decorators
1 parent b2f210f commit 9e12b40
Copy full SHA for 9e12b40

File tree

14 files changed

+565
-62
lines changed
Filter options

14 files changed

+565
-62
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php
+1Lines 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),
+133Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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\Component\DependencyInjection\Exception\InvalidArgumentException;
19+
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
20+
use Symfony\Component\DependencyInjection\Reference;
21+
22+
/**
23+
* Resolves stacks referenced by decorators or by other stacks.
24+
*
25+
* @author Nicolas Grekas <p@tchwork.com>
26+
*/
27+
class ResolveDecoratorStackPass implements CompilerPassInterface
28+
{
29+
private $tag;
30+
31+
public function __construct(string $tag = 'container.stack')
32+
{
33+
$this->tag = $tag;
34+
}
35+
36+
public function process(ContainerBuilder $container)
37+
{
38+
$definitions = $container->getDefinitions();
39+
$stacks = [];
40+
41+
foreach ($container->findTaggedServiceIds($this->tag) as $id => $tags) {
42+
$definition = $container->getDefinition($id);
43+
44+
if (!$definition instanceof ChildDefinition) {
45+
throw new InvalidArgumentException(sprintf('Invalid service "%s": only definitions with a "parent" can have the "%s" tag.', $id, $this->tag));
46+
}
47+
48+
if (!$stack = $definition->getArguments()) {
49+
throw new InvalidArgumentException(sprintf('Invalid service "%s": the stack of decorators is empty.', $id));
50+
}
51+
52+
$stacks[$id] = $stack;
53+
}
54+
55+
if (!$stacks) {
56+
return;
57+
}
58+
59+
$resolvedDefinitions = [];
60+
61+
foreach ($definitions as $id => $definition) {
62+
if (!isset($stacks[$id])) {
63+
$resolvedDefinitions[$id] = $definition;
64+
continue;
65+
}
66+
67+
foreach (array_reverse($this->resolveStack($stacks, [$id]), true) as $k => $v) {
68+
$resolvedDefinitions[$k] = $v;
69+
}
70+
71+
$alias = $container->setAlias($id, $k);
72+
73+
if ($definition->getChanges()['public'] ?? false) {
74+
$alias->setPublic($definition->isPublic());
75+
}
76+
77+
if ($definition->isDeprecated()) {
78+
$alias->setDeprecated(...array_values($definition->getDeprecation('%alias_id%')));
79+
}
80+
}
81+
82+
$container->setDefinitions($resolvedDefinitions);
83+
}
84+
85+
private function resolveStack(array $stacks, array $path): array
86+
{
87+
$definitions = [];
88+
$id = end($path);
89+
$prefix = '.'.$id.'.';
90+
91+
if (!isset($stacks[$id])) {
92+
return ['' => new ChildDefinition($id)];
93+
}
94+
95+
if (key($path) !== $searchKey = array_search($id, $path)) {
96+
throw new ServiceCircularReferenceException($id, \array_slice($path, $searchKey));
97+
}
98+
99+
foreach ($stacks[$id] as $k => $definition) {
100+
if ($definition instanceof ChildDefinition && isset($stacks[$definition->getParent()])) {
101+
$path[] = $definition->getParent();
102+
$definition = unserialize(serialize($definition)); // deep clone
103+
} elseif ($definition instanceof Definition) {
104+
$definitions[$k = $prefix.$k] = $definition;
105+
continue;
106+
} elseif ($definition instanceof Reference || $definition instanceof Alias) {
107+
$path[] = (string) $definition;
108+
} else {
109+
throw new InvalidArgumentException(sprintf('Invalid service "%s": unexpected value of type "%s" found in the stack of decorators.', $id, get_debug_type($definition)));
110+
}
111+
112+
$p = $prefix.$k;
113+
114+
foreach ($this->resolveStack($stacks, $path) as $k => $v) {
115+
$definitions[$p.$k] = $definition instanceof ChildDefinition ? $definition->setParent($k) : new ChildDefinition($k);
116+
$definition = null;
117+
$k = $p.$k;
118+
}
119+
array_pop($path);
120+
}
121+
122+
$decoratedId = $k;
123+
124+
if (1 === \count($path)) {
125+
foreach ($definitions as $k => $definition) {
126+
$definition->setPublic(false)->setTags([])->setDecoratedService($decoratedId);
127+
}
128+
$definition->setDecoratedService(null);
129+
}
130+
131+
return $definitions;
132+
}
133+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractServiceConfigurator.php
+12Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Loader/Configurator/ServicesConfigurator.php
+34Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
+43-56Lines 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('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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.