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
-------
98eeeae3d1
[DI] add syntax to stack decorators
This commit is contained in:
commit
d6b9011ebf
@ -38,6 +38,7 @@ class UnusedTagsPass implements CompilerPassInterface
|
||||
'container.service_locator',
|
||||
'container.service_locator_context',
|
||||
'container.service_subscriber',
|
||||
'container.stack',
|
||||
'controller.argument_value_resolver',
|
||||
'controller.service_arguments',
|
||||
'data_collector',
|
||||
|
@ -51,6 +51,7 @@ class PassConfig
|
||||
$this->optimizationPasses = [[
|
||||
new AutoAliasServicePass(),
|
||||
new ValidateEnvPlaceholdersPass(),
|
||||
new ResolveDecoratorStackPass(),
|
||||
new ResolveChildDefinitionsPass(),
|
||||
new RegisterServiceSubscribersPass(),
|
||||
new ResolveParameterPlaceHoldersPass(false, false),
|
||||
|
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Compiler;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Alias;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class ResolveDecoratorStackPass implements CompilerPassInterface
|
||||
{
|
||||
private $tag;
|
||||
|
||||
public function __construct(string $tag = 'container.stack')
|
||||
{
|
||||
$this->tag = $tag;
|
||||
}
|
||||
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
$stacks = [];
|
||||
|
||||
foreach ($container->findTaggedServiceIds($this->tag) as $id => $tags) {
|
||||
$definition = $container->getDefinition($id);
|
||||
|
||||
if (!$definition instanceof ChildDefinition) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid service "%s": only definitions with a "parent" can have the "%s" tag.', $id, $this->tag));
|
||||
}
|
||||
|
||||
if (!$stack = $definition->getArguments()) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid service "%s": the stack of decorators is empty.', $id));
|
||||
}
|
||||
|
||||
$stacks[$id] = $stack;
|
||||
}
|
||||
|
||||
if (!$stacks) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resolvedDefinitions = [];
|
||||
|
||||
foreach ($container->getDefinitions() as $id => $definition) {
|
||||
if (!isset($stacks[$id])) {
|
||||
$resolvedDefinitions[$id] = $definition;
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (array_reverse($this->resolveStack($stacks, [$id]), true) as $k => $v) {
|
||||
$resolvedDefinitions[$k] = $v;
|
||||
}
|
||||
|
||||
$alias = $container->setAlias($id, $k);
|
||||
|
||||
if ($definition->getChanges()['public'] ?? false) {
|
||||
$alias->setPublic($definition->isPublic());
|
||||
}
|
||||
|
||||
if ($definition->isDeprecated()) {
|
||||
$alias->setDeprecated(...array_values($definition->getDeprecation('%alias_id%')));
|
||||
}
|
||||
}
|
||||
|
||||
$container->setDefinitions($resolvedDefinitions);
|
||||
}
|
||||
|
||||
private function resolveStack(array $stacks, array $path): array
|
||||
{
|
||||
$definitions = [];
|
||||
$id = end($path);
|
||||
$prefix = '.'.$id.'.';
|
||||
|
||||
if (!isset($stacks[$id])) {
|
||||
return [$id => new ChildDefinition($id)];
|
||||
}
|
||||
|
||||
if (key($path) !== $searchKey = array_search($id, $path)) {
|
||||
throw new ServiceCircularReferenceException($id, \array_slice($path, $searchKey));
|
||||
}
|
||||
|
||||
foreach ($stacks[$id] as $k => $definition) {
|
||||
if ($definition instanceof ChildDefinition && isset($stacks[$definition->getParent()])) {
|
||||
$path[] = $definition->getParent();
|
||||
$definition = unserialize(serialize($definition)); // deep clone
|
||||
} elseif ($definition instanceof Definition) {
|
||||
$definitions[$decoratedId = $prefix.$k] = $definition;
|
||||
continue;
|
||||
} elseif ($definition instanceof Reference || $definition instanceof Alias) {
|
||||
$path[] = (string) $definition;
|
||||
} else {
|
||||
throw new InvalidArgumentException(sprintf('Invalid service "%s": unexpected value of type "%s" found in the stack of decorators.', $id, get_debug_type($definition)));
|
||||
}
|
||||
|
||||
$p = $prefix.$k;
|
||||
|
||||
foreach ($this->resolveStack($stacks, $path) as $k => $v) {
|
||||
$definitions[$decoratedId = $p.$k] = $definition instanceof ChildDefinition ? $definition->setParent($k) : new ChildDefinition($k);
|
||||
$definition = null;
|
||||
}
|
||||
array_pop($path);
|
||||
}
|
||||
|
||||
if (1 === \count($path)) {
|
||||
foreach ($definitions as $k => $definition) {
|
||||
$definition->setPublic(false)->setTags([])->setDecoratedService($decoratedId);
|
||||
}
|
||||
$definition->setDecoratedService(null);
|
||||
}
|
||||
|
||||
return $definitions;
|
||||
}
|
||||
}
|
@ -81,6 +81,18 @@ abstract class AbstractServiceConfigurator extends AbstractConfigurator
|
||||
return $this->parent->get($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a stack of decorator services.
|
||||
*
|
||||
* @param InlineServiceConfigurator[]|ReferenceConfigurator[] $services
|
||||
*/
|
||||
final public function stack(string $id, array $services): AliasConfigurator
|
||||
{
|
||||
$this->__destruct();
|
||||
|
||||
return $this->parent->stack($id, $services);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a service.
|
||||
*/
|
||||
|
@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Alias;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
|
||||
@ -131,6 +132,39 @@ class ServicesConfigurator extends AbstractConfigurator
|
||||
return new ServiceConfigurator($this->container, $definition->getInstanceofConditionals(), true, $this, $definition, $id, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a stack of decorator services.
|
||||
*
|
||||
* @param InlineServiceConfigurator[]|ReferenceConfigurator[] $services
|
||||
*/
|
||||
final public function stack(string $id, array $services): AliasConfigurator
|
||||
{
|
||||
foreach ($services as $i => $service) {
|
||||
if ($service instanceof InlineServiceConfigurator) {
|
||||
$definition = $service->definition->setInstanceofConditionals($this->instanceof);
|
||||
|
||||
$changes = $definition->getChanges();
|
||||
$definition->setAutowired((isset($changes['autowired']) ? $definition : $this->defaults)->isAutowired());
|
||||
$definition->setAutoconfigured((isset($changes['autoconfigured']) ? $definition : $this->defaults)->isAutoconfigured());
|
||||
$definition->setBindings(array_merge($this->defaults->getBindings(), $definition->getBindings()));
|
||||
$definition->setChanges($changes);
|
||||
|
||||
$services[$i] = $definition;
|
||||
} elseif (!$service instanceof ReferenceConfigurator) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
$alias = $this->alias($id, '');
|
||||
$alias->definition = $this->set($id)
|
||||
->parent('')
|
||||
->args($services)
|
||||
->tag('container.stack')
|
||||
->definition;
|
||||
|
||||
return $alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a service.
|
||||
*/
|
||||
|
@ -112,12 +112,12 @@ class XmlFileLoader extends FileLoader
|
||||
}
|
||||
}
|
||||
|
||||
private function parseDefinitions(\DOMDocument $xml, string $file, array $defaults)
|
||||
private function parseDefinitions(\DOMDocument $xml, string $file, Definition $defaults)
|
||||
{
|
||||
$xpath = new \DOMXPath($xml);
|
||||
$xpath->registerNamespace('container', self::NS);
|
||||
|
||||
if (false === $services = $xpath->query('//container:services/container:service|//container:services/container:prototype')) {
|
||||
if (false === $services = $xpath->query('//container:services/container:service|//container:services/container:prototype|//container:services/container:stack')) {
|
||||
return;
|
||||
}
|
||||
$this->setCurrentDir(\dirname($file));
|
||||
@ -126,12 +126,34 @@ class XmlFileLoader extends FileLoader
|
||||
$this->isLoadingInstanceof = true;
|
||||
$instanceof = $xpath->query('//container:services/container:instanceof');
|
||||
foreach ($instanceof as $service) {
|
||||
$this->setDefinition((string) $service->getAttribute('id'), $this->parseDefinition($service, $file, []));
|
||||
$this->setDefinition((string) $service->getAttribute('id'), $this->parseDefinition($service, $file, new Definition()));
|
||||
}
|
||||
|
||||
$this->isLoadingInstanceof = false;
|
||||
foreach ($services as $service) {
|
||||
if (null !== $definition = $this->parseDefinition($service, $file, $defaults)) {
|
||||
if ('stack' === $service->tagName) {
|
||||
$service->setAttribute('parent', '-');
|
||||
$definition = $this->parseDefinition($service, $file, $defaults)
|
||||
->setTags(array_merge_recursive(['container.stack' => [[]]], $defaults->getTags()))
|
||||
;
|
||||
$this->setDefinition($id = (string) $service->getAttribute('id'), $definition);
|
||||
$stack = [];
|
||||
|
||||
foreach ($this->getChildren($service, 'service') as $k => $frame) {
|
||||
$k = $frame->getAttribute('id') ?: $k;
|
||||
$frame->setAttribute('id', $id.'" at index "'.$k);
|
||||
|
||||
if ($alias = $frame->getAttribute('alias')) {
|
||||
$this->validateAlias($frame, $file);
|
||||
$stack[$k] = new Reference($alias);
|
||||
} else {
|
||||
$stack[$k] = $this->parseDefinition($frame, $file, $defaults)
|
||||
->setInstanceofConditionals($this->instanceof);
|
||||
}
|
||||
}
|
||||
|
||||
$definition->setArguments($stack);
|
||||
} elseif (null !== $definition = $this->parseDefinition($service, $file, $defaults)) {
|
||||
if ('prototype' === $service->tagName) {
|
||||
$excludes = array_column($this->getChildren($service, 'exclude'), 'nodeValue');
|
||||
if ($service->hasAttribute('exclude')) {
|
||||
@ -148,51 +170,24 @@ class XmlFileLoader extends FileLoader
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service defaults.
|
||||
*/
|
||||
private function getServiceDefaults(\DOMDocument $xml, string $file): array
|
||||
private function getServiceDefaults(\DOMDocument $xml, string $file): Definition
|
||||
{
|
||||
$xpath = new \DOMXPath($xml);
|
||||
$xpath->registerNamespace('container', self::NS);
|
||||
|
||||
if (null === $defaultsNode = $xpath->query('//container:services/container:defaults')->item(0)) {
|
||||
return [];
|
||||
return new Definition();
|
||||
}
|
||||
|
||||
$bindings = [];
|
||||
foreach ($this->getArgumentsAsPhp($defaultsNode, 'bind', $file) as $argument => $value) {
|
||||
$bindings[$argument] = new BoundArgument($value, true, BoundArgument::DEFAULTS_BINDING, $file);
|
||||
}
|
||||
$defaultsNode->setAttribute('id', '<defaults>');
|
||||
|
||||
$defaults = [
|
||||
'tags' => $this->getChildren($defaultsNode, 'tag'),
|
||||
'bind' => $bindings,
|
||||
];
|
||||
|
||||
foreach ($defaults['tags'] as $tag) {
|
||||
if ('' === $tag->getAttribute('name')) {
|
||||
throw new InvalidArgumentException(sprintf('The tag name for tag "<defaults>" in "%s" must be a non-empty string.', $file));
|
||||
}
|
||||
}
|
||||
|
||||
if ($defaultsNode->hasAttribute('autowire')) {
|
||||
$defaults['autowire'] = XmlUtils::phpize($defaultsNode->getAttribute('autowire'));
|
||||
}
|
||||
if ($defaultsNode->hasAttribute('public')) {
|
||||
$defaults['public'] = XmlUtils::phpize($defaultsNode->getAttribute('public'));
|
||||
}
|
||||
if ($defaultsNode->hasAttribute('autoconfigure')) {
|
||||
$defaults['autoconfigure'] = XmlUtils::phpize($defaultsNode->getAttribute('autoconfigure'));
|
||||
}
|
||||
|
||||
return $defaults;
|
||||
return $this->parseDefinition($defaultsNode, $file, new Definition());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an individual Definition.
|
||||
*/
|
||||
private function parseDefinition(\DOMElement $service, string $file, array $defaults): ?Definition
|
||||
private function parseDefinition(\DOMElement $service, string $file, Definition $defaults): ?Definition
|
||||
{
|
||||
if ($alias = $service->getAttribute('alias')) {
|
||||
$this->validateAlias($service, $file);
|
||||
@ -200,8 +195,8 @@ class XmlFileLoader extends FileLoader
|
||||
$this->container->setAlias((string) $service->getAttribute('id'), $alias = new Alias($alias));
|
||||
if ($publicAttr = $service->getAttribute('public')) {
|
||||
$alias->setPublic(XmlUtils::phpize($publicAttr));
|
||||
} elseif (isset($defaults['public'])) {
|
||||
$alias->setPublic($defaults['public']);
|
||||
} elseif ($defaults->getChanges()['public'] ?? false) {
|
||||
$alias->setPublic($defaults->isPublic());
|
||||
}
|
||||
|
||||
if ($deprecated = $this->getChildren($service, 'deprecated')) {
|
||||
@ -231,16 +226,11 @@ class XmlFileLoader extends FileLoader
|
||||
$definition = new Definition();
|
||||
}
|
||||
|
||||
if (isset($defaults['public'])) {
|
||||
$definition->setPublic($defaults['public']);
|
||||
if ($defaults->getChanges()['public'] ?? false) {
|
||||
$definition->setPublic($defaults->isPublic());
|
||||
}
|
||||
if (isset($defaults['autowire'])) {
|
||||
$definition->setAutowired($defaults['autowire']);
|
||||
}
|
||||
if (isset($defaults['autoconfigure'])) {
|
||||
$definition->setAutoconfigured($defaults['autoconfigure']);
|
||||
}
|
||||
|
||||
$definition->setAutowired($defaults->isAutowired());
|
||||
$definition->setAutoconfigured($defaults->isAutoconfigured());
|
||||
$definition->setChanges([]);
|
||||
|
||||
foreach (['class', 'public', 'shared', 'synthetic', 'abstract'] as $key) {
|
||||
@ -324,10 +314,6 @@ class XmlFileLoader extends FileLoader
|
||||
|
||||
$tags = $this->getChildren($service, 'tag');
|
||||
|
||||
if (!empty($defaults['tags'])) {
|
||||
$tags = array_merge($tags, $defaults['tags']);
|
||||
}
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
$parameters = [];
|
||||
foreach ($tag->attributes as $name => $node) {
|
||||
@ -349,16 +335,17 @@ class XmlFileLoader extends FileLoader
|
||||
$definition->addTag($tag->getAttribute('name'), $parameters);
|
||||
}
|
||||
|
||||
$definition->setTags(array_merge_recursive($definition->getTags(), $defaults->getTags()));
|
||||
|
||||
$bindings = $this->getArgumentsAsPhp($service, 'bind', $file);
|
||||
$bindingType = $this->isLoadingInstanceof ? BoundArgument::INSTANCEOF_BINDING : BoundArgument::SERVICE_BINDING;
|
||||
foreach ($bindings as $argument => $value) {
|
||||
$bindings[$argument] = new BoundArgument($value, true, $bindingType, $file);
|
||||
}
|
||||
|
||||
if (isset($defaults['bind'])) {
|
||||
// deep clone, to avoid multiple process of the same instance in the passes
|
||||
$bindings = array_merge(unserialize(serialize($defaults['bind'])), $bindings);
|
||||
}
|
||||
// deep clone, to avoid multiple process of the same instance in the passes
|
||||
$bindings = array_merge(unserialize(serialize($defaults->getBindings())), $bindings);
|
||||
|
||||
if ($bindings) {
|
||||
$definition->setBindings($bindings);
|
||||
}
|
||||
@ -443,7 +430,7 @@ class XmlFileLoader extends FileLoader
|
||||
// resolve definitions
|
||||
uksort($definitions, 'strnatcmp');
|
||||
foreach (array_reverse($definitions) as $id => list($domElement, $file)) {
|
||||
if (null !== $definition = $this->parseDefinition($domElement, $file, [])) {
|
||||
if (null !== $definition = $this->parseDefinition($domElement, $file, new Definition())) {
|
||||
$this->setDefinition($id, $definition);
|
||||
}
|
||||
}
|
||||
|
@ -315,19 +315,20 @@ class YamlFileLoader extends FileLoader
|
||||
*
|
||||
* @throws InvalidArgumentException When tags are invalid
|
||||
*/
|
||||
private function parseDefinition(string $id, $service, string $file, array $defaults)
|
||||
private function parseDefinition(string $id, $service, string $file, array $defaults, bool $return = false)
|
||||
{
|
||||
if (preg_match('/^_[a-zA-Z0-9_]*$/', $id)) {
|
||||
throw new InvalidArgumentException(sprintf('Service names that start with an underscore are reserved. Rename the "%s" service or define it in XML instead.', $id));
|
||||
}
|
||||
|
||||
if (\is_string($service) && 0 === strpos($service, '@')) {
|
||||
$this->container->setAlias($id, $alias = new Alias(substr($service, 1)));
|
||||
$alias = new Alias(substr($service, 1));
|
||||
|
||||
if (isset($defaults['public'])) {
|
||||
$alias->setPublic($defaults['public']);
|
||||
}
|
||||
|
||||
return;
|
||||
return $return ? $alias : $this->container->setAlias($id, $alias);
|
||||
}
|
||||
|
||||
if (\is_array($service) && $this->isUsingShortSyntax($service)) {
|
||||
@ -342,10 +343,52 @@ class YamlFileLoader extends FileLoader
|
||||
throw new InvalidArgumentException(sprintf('A service definition must be an array or a string starting with "@" but "%s" found for service "%s" in "%s". Check your YAML syntax.', get_debug_type($service), $id, $file));
|
||||
}
|
||||
|
||||
if (isset($service['stack'])) {
|
||||
if (!\is_array($service['stack'])) {
|
||||
throw new InvalidArgumentException(sprintf('A stack must be an array of definitions, "%s" given for service "%s" in "%s". Check your YAML syntax.', get_debug_type($service), $id, $file));
|
||||
}
|
||||
|
||||
$stack = [];
|
||||
|
||||
foreach ($service['stack'] as $k => $frame) {
|
||||
if (\is_array($frame) && 1 === \count($frame) && !isset(self::$serviceKeywords[key($frame)])) {
|
||||
$frame = [
|
||||
'class' => key($frame),
|
||||
'arguments' => current($frame),
|
||||
];
|
||||
}
|
||||
|
||||
if (\is_array($frame) && isset($frame['stack'])) {
|
||||
throw new InvalidArgumentException(sprintf('Service stack "%s" cannot contain another stack in "%s".', $id, $file));
|
||||
}
|
||||
|
||||
$definition = $this->parseDefinition($id.'" at index "'.$k, $frame, $file, $defaults, true);
|
||||
|
||||
if ($definition instanceof Definition) {
|
||||
$definition->setInstanceofConditionals($this->instanceof);
|
||||
}
|
||||
|
||||
$stack[$k] = $definition;
|
||||
}
|
||||
|
||||
if ($diff = array_diff(array_keys($service), ['stack', 'public', 'deprecated'])) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid attribute "%s"; supported ones are "public" and "deprecated" for service "%s" in "%s". Check your YAML syntax.', implode('", "', $diff), $id, $file));
|
||||
}
|
||||
|
||||
$service = [
|
||||
'parent' => '',
|
||||
'arguments' => $stack,
|
||||
'tags' => ['container.stack'],
|
||||
'public' => $service['public'] ?? null,
|
||||
'deprecated' => $service['deprecated'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
$this->checkDefinition($id, $service, $file);
|
||||
|
||||
if (isset($service['alias'])) {
|
||||
$this->container->setAlias($id, $alias = new Alias($service['alias']));
|
||||
$alias = new Alias($service['alias']);
|
||||
|
||||
if (isset($service['public'])) {
|
||||
$alias->setPublic($service['public']);
|
||||
} elseif (isset($defaults['public'])) {
|
||||
@ -372,7 +415,7 @@ class YamlFileLoader extends FileLoader
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
return $return ? $alias : $this->container->setAlias($id, $alias);
|
||||
}
|
||||
|
||||
if ($this->isLoadingInstanceof) {
|
||||
@ -426,7 +469,7 @@ class YamlFileLoader extends FileLoader
|
||||
$definition->setAbstract($service['abstract']);
|
||||
}
|
||||
|
||||
if (\array_key_exists('deprecated', $service)) {
|
||||
if (isset($service['deprecated'])) {
|
||||
$deprecation = \is_array($service['deprecated']) ? $service['deprecated'] : ['message' => $service['deprecated']];
|
||||
|
||||
if (!isset($deprecation['package'])) {
|
||||
@ -601,6 +644,14 @@ class YamlFileLoader extends FileLoader
|
||||
throw new InvalidArgumentException(sprintf('A "resource" attribute must be set when the "namespace" attribute is set for service "%s" in "%s". Check your YAML syntax.', $id, $file));
|
||||
}
|
||||
|
||||
if ($return) {
|
||||
if (\array_key_exists('resource', $service)) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid "resource" attribute found for service "%s" in "%s". Check your YAML syntax.', $id, $file));
|
||||
}
|
||||
|
||||
return $definition;
|
||||
}
|
||||
|
||||
if (\array_key_exists('resource', $service)) {
|
||||
if (!\is_string($service['resource'])) {
|
||||
throw new InvalidArgumentException(sprintf('A "resource" attribute must be of type string for service "%s" in "%s". Check your YAML syntax.', $id, $file));
|
||||
|
@ -57,6 +57,7 @@
|
||||
<xsd:element name="prototype" type="prototype" minOccurs="0" />
|
||||
<xsd:element name="defaults" type="defaults" minOccurs="0" maxOccurs="1" />
|
||||
<xsd:element name="instanceof" type="instanceof" minOccurs="0" />
|
||||
<xsd:element name="stack" type="stack" minOccurs="0" />
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
|
||||
@ -176,6 +177,15 @@
|
||||
<xsd:attribute name="autoconfigure" type="boolean" />
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="stack">
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="service" type="service" minOccurs="1" />
|
||||
<xsd:element name="deprecated" type="deprecated" minOccurs="0" maxOccurs="1" />
|
||||
</xsd:choice>
|
||||
<xsd:attribute name="id" type="xsd:string" use="required" />
|
||||
<xsd:attribute name="public" type="boolean" />
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="tag">
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
<xsd:anyAttribute namespace="##any" processContents="lax" />
|
||||
|
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo;
|
||||
|
||||
return function (ContainerConfigurator $c) {
|
||||
$services = $c->services();
|
||||
|
||||
$services->stack('stack_a', [
|
||||
service('stdClass')
|
||||
->property('label', 'A')
|
||||
->property('inner', ref('.inner')),
|
||||
service('stdClass')
|
||||
->property('label', 'B')
|
||||
->property('inner', ref('.inner')),
|
||||
service('stdClass')
|
||||
->property('label', 'C'),
|
||||
])->public();
|
||||
|
||||
$services->stack('stack_abstract', [
|
||||
service('stdClass')
|
||||
->property('label', 'A')
|
||||
->property('inner', ref('.inner')),
|
||||
service('stdClass')
|
||||
->property('label', 'B')
|
||||
->property('inner', ref('.inner')),
|
||||
]);
|
||||
|
||||
$services->stack('stack_b', [
|
||||
ref('stack_abstract'),
|
||||
service('stdClass')
|
||||
->property('label', 'C'),
|
||||
])->public();
|
||||
|
||||
$services->stack('stack_c', [
|
||||
service('stdClass')
|
||||
->property('label', 'Z')
|
||||
->property('inner', ref('.inner')),
|
||||
ref('stack_a'),
|
||||
])->public();
|
||||
|
||||
$services->stack('stack_d', [
|
||||
service()
|
||||
->parent('stack_abstract')
|
||||
->property('label', 'Z'),
|
||||
service('stdClass')
|
||||
->property('label', 'C'),
|
||||
])->public();
|
||||
};
|
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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">
|
||||
<services>
|
||||
<stack id="stack_a" public="true">
|
||||
<service class="stdClass">
|
||||
<property name="label">A</property>
|
||||
<property name="inner" type="service" id=".inner" />
|
||||
</service>
|
||||
<service class="stdClass">
|
||||
<property name="label">B</property>
|
||||
<property name="inner" type="service" id=".inner" />
|
||||
</service>
|
||||
<service class="stdClass">
|
||||
<property name="label">C</property>
|
||||
</service>
|
||||
</stack>
|
||||
|
||||
<stack id="stack_abstract">
|
||||
<service class="stdClass" abstract="true">
|
||||
<property name="label">A</property>
|
||||
<property name="inner" type="service" id=".inner" />
|
||||
</service>
|
||||
<service class="stdClass">
|
||||
<property name="label">B</property>
|
||||
<property name="inner" type="service" id=".inner" />
|
||||
</service>
|
||||
</stack>
|
||||
|
||||
<stack id="stack_b" public="true">
|
||||
<service alias="stack_abstract" />
|
||||
<service class="stdClass">
|
||||
<property name="label">C</property>
|
||||
</service>
|
||||
</stack>
|
||||
|
||||
<stack id="stack_c" public="true">
|
||||
<service class="stdClass">
|
||||
<property name="label">Z</property>
|
||||
<property name="inner" type="service" id=".inner" />
|
||||
</service>
|
||||
<service alias="stack_a" />
|
||||
</stack>
|
||||
|
||||
<stack id="stack_d" public="true">
|
||||
<service parent="stack_abstract">
|
||||
<property name="label">Z</property>
|
||||
</service>
|
||||
<service class="stdClass">
|
||||
<property name="label">C</property>
|
||||
</service>
|
||||
</stack>
|
||||
</services>
|
||||
</container>
|
@ -0,0 +1,67 @@
|
||||
services:
|
||||
stack_short:
|
||||
stack:
|
||||
- stdClass: [1, 2]
|
||||
|
||||
stack_a:
|
||||
public: true
|
||||
stack:
|
||||
- class: stdClass
|
||||
properties:
|
||||
label: A
|
||||
inner: '@.inner'
|
||||
- class: stdClass
|
||||
properties:
|
||||
label: B
|
||||
inner: '@.inner'
|
||||
- class: stdClass
|
||||
properties:
|
||||
label: C
|
||||
|
||||
stack_abstract:
|
||||
stack:
|
||||
- class: stdClass
|
||||
abstract: true
|
||||
properties:
|
||||
label: A
|
||||
inner: '@.inner'
|
||||
- class: stdClass
|
||||
properties:
|
||||
label: B
|
||||
inner: '@.inner'
|
||||
|
||||
stack_b:
|
||||
public: true
|
||||
stack:
|
||||
- alias: 'stack_abstract'
|
||||
- class: stdClass
|
||||
properties:
|
||||
label: C
|
||||
|
||||
stack_c:
|
||||
public: true
|
||||
stack:
|
||||
- class: stdClass
|
||||
properties:
|
||||
label: Z
|
||||
inner: '@.inner'
|
||||
- '@stack_a'
|
||||
|
||||
stack_d:
|
||||
public: true
|
||||
stack:
|
||||
- parent: 'stack_abstract'
|
||||
properties:
|
||||
label: 'Z'
|
||||
- class: stdClass
|
||||
properties:
|
||||
label: C
|
||||
|
||||
stack_e:
|
||||
public: true
|
||||
stack:
|
||||
- class: stdClass
|
||||
properties:
|
||||
label: Y
|
||||
inner: '@.inner'
|
||||
- '@stack_d'
|
@ -104,6 +104,38 @@ class PhpFileLoaderTest extends TestCase
|
||||
$container->compile();
|
||||
}
|
||||
|
||||
public function testStack()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$loader = new PhpFileLoader($container, new FileLocator(realpath(__DIR__.'/../Fixtures').'/config'));
|
||||
$loader->load('stack.php');
|
||||
|
||||
$container->compile();
|
||||
|
||||
$expected = (object) [
|
||||
'label' => 'A',
|
||||
'inner' => (object) [
|
||||
'label' => 'B',
|
||||
'inner' => (object) [
|
||||
'label' => 'C',
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($expected, $container->get('stack_a'));
|
||||
$this->assertEquals($expected, $container->get('stack_b'));
|
||||
|
||||
$expected = (object) [
|
||||
'label' => 'Z',
|
||||
'inner' => $expected,
|
||||
];
|
||||
$this->assertEquals($expected, $container->get('stack_c'));
|
||||
|
||||
$expected = $expected->inner;
|
||||
$expected->label = 'Z';
|
||||
$this->assertEquals($expected, $container->get('stack_d'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
*/
|
||||
|
@ -1044,4 +1044,36 @@ class XmlFileLoaderTest extends TestCase
|
||||
$arguments = $container->getDefinition(FooWithAbstractArgument::class)->getArguments();
|
||||
$this->assertInstanceOf(AbstractArgument::class, $arguments['$baz']);
|
||||
}
|
||||
|
||||
public function testStack()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
|
||||
$loader->load('stack.xml');
|
||||
|
||||
$container->compile();
|
||||
|
||||
$expected = (object) [
|
||||
'label' => 'A',
|
||||
'inner' => (object) [
|
||||
'label' => 'B',
|
||||
'inner' => (object) [
|
||||
'label' => 'C',
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($expected, $container->get('stack_a'));
|
||||
$this->assertEquals($expected, $container->get('stack_b'));
|
||||
|
||||
$expected = (object) [
|
||||
'label' => 'Z',
|
||||
'inner' => $expected,
|
||||
];
|
||||
$this->assertEquals($expected, $container->get('stack_c'));
|
||||
|
||||
$expected = $expected->inner;
|
||||
$expected->label = 'Z';
|
||||
$this->assertEquals($expected, $container->get('stack_d'));
|
||||
}
|
||||
}
|
||||
|
@ -962,4 +962,44 @@ class YamlFileLoaderTest extends TestCase
|
||||
|
||||
$this->assertSame($expected, $container->getDefinition('foo')->getMethodCalls());
|
||||
}
|
||||
|
||||
public function testStack()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
|
||||
$loader->load('stack.yaml');
|
||||
|
||||
$this->assertSame([1, 2], $container->getDefinition('stack_short')->getArguments()[0]->getArguments());
|
||||
|
||||
$container->compile();
|
||||
|
||||
$expected = (object) [
|
||||
'label' => 'A',
|
||||
'inner' => (object) [
|
||||
'label' => 'B',
|
||||
'inner' => (object) [
|
||||
'label' => 'C',
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($expected, $container->get('stack_a'));
|
||||
$this->assertEquals($expected, $container->get('stack_b'));
|
||||
|
||||
$expected = (object) [
|
||||
'label' => 'Z',
|
||||
'inner' => $expected,
|
||||
];
|
||||
$this->assertEquals($expected, $container->get('stack_c'));
|
||||
|
||||
$expected = $expected->inner;
|
||||
$expected->label = 'Z';
|
||||
$this->assertEquals($expected, $container->get('stack_d'));
|
||||
|
||||
$expected = (object) [
|
||||
'label' => 'Y',
|
||||
'inner' => $expected,
|
||||
];
|
||||
$this->assertEquals($expected, $container->get('stack_e'));
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user