From 9b7138545eefcea6dcb2601c1f5644ad50e14e13 Mon Sep 17 00:00:00 2001 From: Guilhem Niot Date: Fri, 10 Mar 2017 19:53:14 +0100 Subject: [PATCH] [DependencyInjection] Support anonymous services in Yaml --- .../DependencyInjection/CHANGELOG.md | 1 + .../Loader/YamlFileLoader.php | 59 +++++++++---- .../Fixtures/yaml/anonymous_services.yml | 14 ++++ .../yaml/anonymous_services_alias.yml | 7 ++ .../yaml/anonymous_services_in_instanceof.yml | 14 ++++ .../yaml/anonymous_services_in_parameters.yml | 2 + .../Tests/Loader/YamlFileLoaderTest.php | 82 +++++++++++++++++++ 7 files changed, 164 insertions(+), 15 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services.yml create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_alias.yml create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_in_instanceof.yml create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_in_parameters.yml diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 57128a39ebab7..c61741500fdb0 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -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 diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index f35d82aa9f97b..4de60a94657de 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -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 */ class YamlFileLoader extends FileLoader @@ -107,6 +105,8 @@ class YamlFileLoader extends FileLoader private $yamlParser; + private $anonymousServicesCount; + /** * {@inheritdoc} */ @@ -133,7 +133,7 @@ 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)); } } @@ -141,6 +141,7 @@ public function load($resource, $type = null) $this->loadFromExtensions($content); // services + $this->anonymousServicesCount = 0; $this->setCurrentDir(dirname($path)); try { $this->parseDefinitions($content, $resource); @@ -416,11 +417,11 @@ 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'])) { @@ -428,7 +429,7 @@ private function parseDefinition($id, $service, $file, array $defaults) } if (isset($service['getters'])) { - $definition->setOverriddenGetters($this->resolveServices($service['getters'])); + $definition->setOverriddenGetters($this->resolveServices($service['getters'], $file)); } if (isset($service['calls'])) { @@ -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); @@ -553,7 +554,7 @@ 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; @@ -561,7 +562,7 @@ private function parseCallable($callable, $parameter, $id, $file) 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]) { @@ -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(); @@ -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)) { @@ -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], '@@')) { @@ -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, '@')) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services.yml new file mode 100644 index 0000000000000..fe54b8987e7f3 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services.yml @@ -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' ] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_alias.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_alias.yml new file mode 100644 index 0000000000000..96546b83ac41c --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_alias.yml @@ -0,0 +1,7 @@ +services: + Bar: ~ + + Foo: + arguments: + - !service + alias: Bar diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_in_instanceof.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_in_instanceof.yml new file mode 100644 index 0000000000000..a45a73b993349 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_in_instanceof.yml @@ -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: ~ diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_in_parameters.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_in_parameters.yml new file mode 100644 index 0000000000000..9d9bea344efec --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_in_parameters.yml @@ -0,0 +1,2 @@ +parameters: + foo: [ !service { } ] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index ed06644d5207c..3c69c50c8679c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -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