Skip to content

[DependencyInjection] Support anonymous services in Yaml #21970

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
3.3.0
-----

* added anonymous services support in YAML configuration files using the `!service` tag.
* [EXPERIMENTAL] added "TypedReference" and "ServiceClosureArgument" for creating service-locator services
* [EXPERIMENTAL] added "instanceof" section for local interface-defined configs
* added "service-locator" argument for lazy loading a set of identified values and services
Expand Down
59 changes: 44 additions & 15 deletions src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@
/**
* YamlFileLoader loads YAML files service definitions.
*
* The YAML format does not support anonymous services (cf. the XML loader).
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class YamlFileLoader extends FileLoader
Expand Down Expand Up @@ -107,6 +105,8 @@ class YamlFileLoader extends FileLoader

private $yamlParser;

private $anonymousServicesCount;

/**
* {@inheritdoc}
*/
Expand All @@ -133,14 +133,15 @@ public function load($resource, $type = null)
}

foreach ($content['parameters'] as $key => $value) {
$this->container->setParameter($key, $this->resolveServices($value));
$this->container->setParameter($key, $this->resolveServices($value, $resource, true));
}
}

// extensions
$this->loadFromExtensions($content);

// services
$this->anonymousServicesCount = 0;
$this->setCurrentDir(dirname($path));
try {
$this->parseDefinitions($content, $resource);
Expand Down Expand Up @@ -416,19 +417,19 @@ private function parseDefinition($id, $service, $file, array $defaults)
}

if (isset($service['arguments'])) {
$definition->setArguments($this->resolveServices($service['arguments']));
$definition->setArguments($this->resolveServices($service['arguments'], $file));
}

if (isset($service['properties'])) {
$definition->setProperties($this->resolveServices($service['properties']));
$definition->setProperties($this->resolveServices($service['properties'], $file));
}

if (isset($service['configurator'])) {
$definition->setConfigurator($this->parseCallable($service['configurator'], 'configurator', $id, $file));
}

if (isset($service['getters'])) {
$definition->setOverriddenGetters($this->resolveServices($service['getters']));
$definition->setOverriddenGetters($this->resolveServices($service['getters'], $file));
}

if (isset($service['calls'])) {
Expand All @@ -439,10 +440,10 @@ private function parseDefinition($id, $service, $file, array $defaults)
foreach ($service['calls'] as $call) {
if (isset($call['method'])) {
$method = $call['method'];
$args = isset($call['arguments']) ? $this->resolveServices($call['arguments']) : array();
$args = isset($call['arguments']) ? $this->resolveServices($call['arguments'], $file) : array();
} else {
$method = $call[0];
$args = isset($call[1]) ? $this->resolveServices($call[1]) : array();
$args = isset($call[1]) ? $this->resolveServices($call[1], $file) : array();
}

$definition->addMethodCall($method, $args);
Expand Down Expand Up @@ -553,15 +554,15 @@ private function parseCallable($callable, $parameter, $id, $file)
if (false !== strpos($callable, ':') && false === strpos($callable, '::')) {
$parts = explode(':', $callable);

return array($this->resolveServices('@'.$parts[0]), $parts[1]);
return array($this->resolveServices('@'.$parts[0], $file), $parts[1]);
}

return $callable;
}

if (is_array($callable)) {
if (isset($callable[0]) && isset($callable[1])) {
return array($this->resolveServices($callable[0]), $callable[1]);
return array($this->resolveServices($callable[0], $file), $callable[1]);
}

if ('factory' === $parameter && isset($callable[1]) && null === $callable[0]) {
Expand Down Expand Up @@ -653,11 +654,13 @@ private function validate($content, $file)
/**
* Resolves services.
*
* @param mixed $value
* @param mixed $value
* @param string $file
* @param bool $isParameter
*
* @return array|string|Reference|ArgumentInterface
*/
private function resolveServices($value)
private function resolveServices($value, $file, $isParameter = false)
{
if ($value instanceof TaggedValue) {
$argument = $value->getValue();
Expand All @@ -666,7 +669,7 @@ private function resolveServices($value)
throw new InvalidArgumentException('"!iterator" tag only accepts sequences.');
}

return new IteratorArgument($this->resolveServices($argument));
return new IteratorArgument($this->resolveServices($argument, $file, $isParameter));
}
if ('service_locator' === $value->getTag()) {
if (!is_array($argument)) {
Expand All @@ -679,7 +682,7 @@ private function resolveServices($value)
}
}

return new ServiceLocatorArgument($this->resolveServices($argument));
return new ServiceLocatorArgument($this->resolveServices($argument, $file, $isParameter));
}
if ('closure_proxy' === $value->getTag()) {
if (!is_array($argument) || array(0, 1) !== array_keys($argument) || !is_string($argument[0]) || !is_string($argument[1]) || 0 !== strpos($argument[0], '@') || 0 === strpos($argument[0], '@@')) {
Expand All @@ -696,12 +699,38 @@ private function resolveServices($value)

return new ClosureProxyArgument($argument[0], $argument[1], $invalidBehavior);
}
if ('service' === $value->getTag()) {
if ($isParameter) {
throw new InvalidArgumentException(sprintf('Using an anonymous service in a parameter is not allowed in "%s".', $file));
}

$isLoadingInstanceof = $this->isLoadingInstanceof;
$this->isLoadingInstanceof = false;
$instanceof = $this->instanceof;
$this->instanceof = array();

$id = sprintf('%d_%s', ++$this->anonymousServicesCount, hash('sha256', $file));
$this->parseDefinition($id, $argument, $file, array());

if (!$this->container->hasDefinition($id)) {
throw new InvalidArgumentException(sprintf('Creating an alias using the tag "!service" is not allowed in "%s".', $file));
}

$this->container->getDefinition($id)->setPublic(false);

$this->isLoadingInstanceof = $isLoadingInstanceof;
$this->instanceof = $instanceof;

return new Reference($id);
}

throw new InvalidArgumentException(sprintf('Unsupported tag "!%s".', $value->getTag()));
}

if (is_array($value)) {
$value = array_map(array($this, 'resolveServices'), $value);
foreach ($value as $k => $v) {
$value[$k] = $this->resolveServices($v, $file, $isParameter);
}
} elseif (is_string($value) && 0 === strpos($value, '@=')) {
return new Expression(substr($value, 2));
} elseif (is_string($value) && 0 === strpos($value, '@')) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
imports:
# Ensure the anonymous services count is reset after importing a file
- { resource: anonymous_services_in_instanceof.yml }

services:
_defaults:
autowire: true

Foo:
arguments:
- !service
class: Bar
autowire: true
factory: [ !service { class: Quz }, 'constructFoo' ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
Bar: ~

Foo:
arguments:
- !service
alias: Bar
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
services:
_instanceof:
# Ensure previous conditionals aren't applied on anonymous services
Quz:
autowire: true

DummyInterface:
arguments: [ !service { class: Anonymous } ]

# Ensure next conditionals are not considered as services
Bar:
autowire: true

Dummy: ~
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
parameters:
foo: [ !service { } ]
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,88 @@ public function testUnderscoreServiceId()
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('services_underscore.yml');
}

public function testAnonymousServices()
{
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('anonymous_services.yml');

$definition = $container->getDefinition('Foo');
$this->assertTrue($definition->isAutowired());

// Anonymous service in an argument
$args = $definition->getArguments();
$this->assertCount(1, $args);
$this->assertInstanceOf(Reference::class, $args[0]);
$this->assertTrue($container->has((string) $args[0]));
$this->assertStringStartsWith('2', (string) $args[0]);

$anonymous = $container->getDefinition((string) $args[0]);
$this->assertEquals('Bar', $anonymous->getClass());
$this->assertFalse($anonymous->isPublic());
$this->assertTrue($anonymous->isAutowired());

// Anonymous service in a callable
$factory = $definition->getFactory();
$this->assertInternalType('array', $factory);
$this->assertInstanceOf(Reference::class, $factory[0]);
$this->assertTrue($container->has((string) $factory[0]));
$this->assertStringStartsWith('1', (string) $factory[0]);
$this->assertEquals('constructFoo', $factory[1]);

$anonymous = $container->getDefinition((string) $factory[0]);
$this->assertEquals('Quz', $anonymous->getClass());
$this->assertFalse($anonymous->isPublic());
$this->assertFalse($anonymous->isAutowired());
}

public function testAnonymousServicesInInstanceof()
{
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('anonymous_services_in_instanceof.yml');

$definition = $container->getDefinition('Dummy');

$instanceof = $definition->getInstanceofConditionals();
$this->assertCount(3, $instanceof);
$this->assertArrayHasKey('DummyInterface', $instanceof);

$args = $instanceof['DummyInterface']->getArguments();
$this->assertCount(1, $args);
$this->assertInstanceOf(Reference::class, $args[0]);
$this->assertTrue($container->has((string) $args[0]));

$anonymous = $container->getDefinition((string) $args[0]);
$this->assertEquals('Anonymous', $anonymous->getClass());
$this->assertFalse($anonymous->isPublic());
$this->assertEmpty($anonymous->getInstanceofConditionals());

$this->assertFalse($container->has('Bar'));
}

/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* @expectedExceptionMessage Creating an alias using the tag "!service" is not allowed in "anonymous_services_alias.yml".
*/
public function testAnonymousServicesWithAliases()
{
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('anonymous_services_alias.yml');
}

/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* @expectedExceptionMessage Using an anonymous service in a parameter is not allowed in "anonymous_services_in_parameters.yml".
*/
public function testAnonymousServicesInParameters()
{
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('anonymous_services_in_parameters.yml');
}
}

interface FooInterface
Expand Down