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 d6b9011

Browse filesBrowse files
committed
feature #36373 [DI] add syntax to stack decorators (nicolas-grekas)
This PR was merged into the 5.1-dev branch. Discussion ---------- [DI] add syntax to stack decorators | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix #30599 | License | MIT | Doc PR | - Declare this: ```yaml services: my_stack_of_decorators: stack: - class: App\ExternalDecorator - class: App\InternalDecorator - class: App\DecoratoredClass ``` And get this: ![image](https://user-images.githubusercontent.com/243674/78615803-b8c8e580-7872-11ea-95c2-22cb78f88ca8.png) The PR is now ready with support for Yaml, XML and the PHP-DSL. It needs #36388, #36392 and #36389 to pass, and relates to #36390 to be DX-friendly. The new syntax now supports composable stacks - i.e stack you can reuse in the middle of another stack. RIP middleware, simple decorators FTW :) From the test cases: ```yaml services: reusable_stack: stack: - class: stdClass properties: label: A inner: '@.inner' - class: stdClass properties: label: B inner: '@.inner' concrete_stack: stack: - parent: reusable_stack - class: stdClass properties: label: C ``` This will create a service similar to: ```php (object) [ 'label' => 'A', 'inner' => (object) [ 'label' => 'B', 'inner' => (object) [ 'label' => 'C', ] ], ]; ``` When used together with autowiring, this is enough to declare a stack of decorators: ```yaml services: my_processing_stack: stack: - App\ExternalDecorator: ~ - App\InternalDecorator: ~ - App\TheDecoratedClass: ~ ``` See fixtures for the other configuration formats. See also https://twitter.com/nicolasgrekas/status/1248198573998604288 Todo: - [x] rebase on top of #36388, #36392 and #36389 once they are merged - [x] test declaring deeper nested stacks Commits ------- 98eeeae [DI] add syntax to stack decorators
2 parents 9d763e0 + 98eeeae commit d6b9011
Copy full SHA for d6b9011

File tree

14 files changed

+559
-62
lines changed
Filter options

14 files changed

+559
-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),
+127Lines 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\Component\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

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.