diff --git a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
index 90cd6bcfafa4d..3002975db3371 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
@@ -34,4 +34,18 @@ public function __construct(ContainerBuilder $container, FileLocatorInterface $l
parent::__construct($locator);
}
+
+ /**
+ * Generates the ID of an "instanceof" definition.
+ *
+ * @param string $id
+ * @param string $type
+ * @param string $file
+ *
+ * @return string
+ */
+ protected function generateInstanceofDefinitionId($id, $type, $file)
+ {
+ return sprintf('0%s_%s_%s', $id, $type, hash('sha256', $file));
+ }
}
diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
index d428c1399b871..73f82986138c0 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
@@ -126,8 +126,9 @@ private function parseDefinitions(\DOMDocument $xml, $file)
}
$defaults = $this->getServiceDefaults($xml, $file);
+ $instanceof = $xpath->query('//container:services/container:instanceof/container:service') ?: null;
foreach ($services as $service) {
- if (null !== $definition = $this->parseDefinition($service, $file, $defaults)) {
+ if (null !== $definition = $this->parseDefinition($service, $file, $defaults, $instanceof)) {
$this->container->setDefinition((string) $service->getAttribute('id'), $definition);
}
}
@@ -182,14 +183,16 @@ private function getServiceDefaults(\DOMDocument $xml, $file)
/**
* Parses an individual Definition.
*
- * @param \DOMElement $service
- * @param string $file
- * @param array $defaults
+ * @param \DOMElement $service
+ * @param string $file
+ * @param array $defaults
+ * @param \DOMNodeList|null $instanceof
*
* @return Definition|null
*/
- private function parseDefinition(\DOMElement $service, $file, array $defaults = array())
+ private function parseDefinition(\DOMElement $service, $file, array $defaults = array(), \DOMNodeList $instanceof = null)
{
+ $id = (string) $service->getAttribute('id');
if ($alias = $service->getAttribute('alias')) {
$this->validateAlias($service, $file);
@@ -199,11 +202,48 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults =
} elseif (isset($defaults['public'])) {
$public = $defaults['public'];
}
- $this->container->setAlias((string) $service->getAttribute('id'), new Alias($alias, $public));
+ $this->container->setAlias($id, new Alias($alias, $public));
return;
}
+ $definition = $this->getDefinition($service, $file, $defaults);
+ if (null === $instanceof) {
+ return $definition;
+ }
+
+ $className = $definition->getClass() ?: $id;
+ $parentId = $definition instanceof ChildDefinition ? $definition->getParent() : null;
+ foreach ($instanceof as $s) {
+ $type = (string) $s->getAttribute('id');
+ if (!is_a($className, $type, true)) {
+ continue;
+ }
+
+ if ($parentId) {
+ $s = clone $s;
+ $s->setAttribute('parent', $parentId);
+ }
+ $parentId = $this->generateInstanceofDefinitionId($id, $type, $file);
+ $parentDefinition = $this->getDefinition($s, $file);
+ $parentDefinition->setAbstract(true);
+ if ($parentDefinition instanceof ChildDefinition) {
+ $definition->setInheritTags(true);
+ }
+ $this->container->setDefinition($parentId, $parentDefinition);
+ }
+
+ if (null !== $parentId) {
+ $service->setAttribute('parent', $parentId);
+ $definition = $this->getDefinition($service, $file, $defaults);
+ $definition->setInheritTags(true);
+ }
+
+ return $definition;
+ }
+
+ private function getDefinition(\DOMElement $service, $file, array $defaults = array())
+ {
if ($parent = $service->getAttribute('parent')) {
$definition = new ChildDefinition($parent);
diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
index df7d959cbef55..9517fdb390016 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
@@ -159,8 +159,16 @@ private function parseDefinitions(array $content, $file)
}
$defaults = $this->parseDefaults($content, $file);
+
+ if ($this->isUnderscoredParamValid($content, '_instanceof', $file)) {
+ $instanceof = $content['services']['_instanceof'];
+ unset($content['services']['_instanceof']);
+ } else {
+ $instanceof = array();
+ }
+
foreach ($content['services'] as $id => $service) {
- $this->parseDefinition($id, $service, $file, $defaults);
+ $this->parseDefinition($id, $service, $file, $defaults, $instanceof);
}
}
@@ -174,20 +182,13 @@ private function parseDefinitions(array $content, $file)
*/
private function parseDefaults(array &$content, $file)
{
- if (!isset($content['services']['_defaults'])) {
- return $content;
- }
- if (!is_array($defaults = $content['services']['_defaults'])) {
- throw new InvalidArgumentException(sprintf('Service defaults must be an array, "%s" given in "%s".', gettype($defaults), $file));
+ if (!$this->isUnderscoredParamValid($content, '_defaults', $file)) {
+ return array();
}
- if (isset($defaults['alias']) || isset($defaults['class']) || isset($defaults['factory'])) {
- @trigger_error('Giving a service the "_defaults" name is deprecated since Symfony 3.3 and will be forbidden in 4.0. Rename your service.', E_USER_DEPRECATED);
- return $content;
- }
-
- $defaultKeys = array('public', 'tags', 'inherit_tags', 'autowire');
+ $defaults = $content['services']['_defaults'];
unset($content['services']['_defaults']);
+ $defaultKeys = array('public', 'tags', 'inherit_tags', 'autowire');
foreach ($defaults as $key => $default) {
if (!in_array($key, $defaultKeys)) {
@@ -226,6 +227,25 @@ private function parseDefaults(array &$content, $file)
return $defaults;
}
+ private function isUnderscoredParamValid($content, $name, $file)
+ {
+ if (!isset($content['services'][$name])) {
+ return false;
+ }
+
+ if (!is_array($underscoreParam = $content['services'][$name])) {
+ throw new InvalidArgumentException(sprintf('Service "%s" key must be an array, "%s" given in "%s".', $name, gettype($underscoreParam), $file));
+ }
+
+ if (isset($underscoreParam['alias']) || isset($underscoreParam['class']) || isset($underscoreParam['factory'])) {
+ @trigger_error(sprintf('Giving a service the "%s" name is deprecated since Symfony 3.3 and will be forbidden in 4.0. Rename your service.', $name), E_USER_DEPRECATED);
+
+ return false;
+ }
+
+ return true;
+ }
+
/**
* Parses a definition.
*
@@ -233,10 +253,11 @@ private function parseDefaults(array &$content, $file)
* @param array|string $service
* @param string $file
* @param array $defaults
+ * @param array $instanceof
*
* @throws InvalidArgumentException When tags are invalid
*/
- private function parseDefinition($id, $service, $file, array $defaults)
+ private function parseDefinition($id, $service, $file, array $defaults, array $instanceof)
{
if (is_string($service) && 0 === strpos($service, '@')) {
$public = isset($defaults['public']) ? $defaults['public'] : true;
@@ -253,12 +274,6 @@ private function parseDefinition($id, $service, $file, array $defaults)
$service = array();
}
- if (!is_array($service)) {
- 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.', gettype($service), $id, $file));
- }
-
- static::checkDefinition($id, $service, $file);
-
if (isset($service['alias'])) {
$public = array_key_exists('public', $service) ? (bool) $service['public'] : (isset($defaults['public']) ? $defaults['public'] : true);
$this->container->setAlias($id, new Alias($service['alias'], $public));
@@ -272,6 +287,43 @@ private function parseDefinition($id, $service, $file, array $defaults)
return;
}
+ $definition = $this->getDefinition($id, $service, $file, $defaults);
+ $className = $definition->getClass() ?: $id;
+ $parentId = $definition instanceof ChildDefinition ? $definition->getParent() : null;
+ foreach ($instanceof as $type => $value) {
+ if (!is_a($className, $type, true)) {
+ continue;
+ }
+
+ if ($parentId) {
+ $value['parent'] = $parentId;
+ }
+ $parentId = $this->generateInstanceofDefinitionId($id, $type, $file);
+ $parentDefinition = $this->getDefinition($parentId, $value, $file, array());
+ $parentDefinition->setAbstract(true);
+ if ($parentDefinition instanceof ChildDefinition) {
+ $definition->setInheritTags(true);
+ }
+ $this->container->setDefinition($parentId, $parentDefinition);
+ }
+
+ if (null !== $parentId) {
+ $service['parent'] = $parentId;
+ $definition = $this->getDefinition($id, $service, $file, $defaults);
+ $definition->setInheritTags(true);
+ }
+
+ $this->container->setDefinition($id, $definition);
+ }
+
+ private function getDefinition($id, $service, $file, array $defaults)
+ {
+ if (!is_array($service)) {
+ 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.', gettype($service), $id, $file));
+ }
+
+ static::checkDefinition($id, $service, $file);
+
if (isset($service['parent'])) {
$definition = new ChildDefinition($service['parent']);
@@ -430,7 +482,7 @@ private function parseDefinition($id, $service, $file, array $defaults)
}
}
- $this->container->setDefinition($id, $definition);
+ return $definition;
}
/**
diff --git a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd
index 02b0187b80c16..0efcc10463d22 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd
+++ b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd
@@ -55,6 +55,7 @@
+
@@ -135,6 +136,12 @@
+
+
+
+
+
+
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services32.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services32.xml
new file mode 100644
index 0000000000000..5bb4a11caf026
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services32.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+ __construct
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services32.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services32.yml
new file mode 100644
index 0000000000000..f0612c6d0a684
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services32.yml
@@ -0,0 +1,10 @@
+services:
+ _instanceof:
+ Symfony\Component\DependencyInjection\Tests\Loader\FooInterface:
+ autowire: true
+ lazy: true
+ tags:
+ - { name: foo }
+ - { name: bar }
+
+ Symfony\Component\DependencyInjection\Tests\Loader\Foo: ~
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
index a1b95616805e4..bc98815c511de 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Tests\Loader;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
+use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
@@ -658,4 +659,33 @@ public function testDefaultsWithAutowiredMethods()
$this->assertSame(array('setFoo'), $container->getDefinition('no_defaults_child')->getAutowiredMethods());
$this->assertSame(array(), $container->getDefinition('with_defaults_child')->getAutowiredMethods());
}
+
+ public function testInstanceof()
+ {
+ $container = new ContainerBuilder();
+ $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
+ $loader->load('services32.xml');
+
+ $definitions = $container->getDefinitions();
+ $parentDefinition = array_shift($definitions);
+ $this->assertNotInstanceOf(ChildDefinition::class, $parentDefinition);
+ $this->assertTrue($parentDefinition->isAutowired());
+ $this->assertTrue($parentDefinition->isLazy());
+ $this->assertEquals(array('foo' => array(array()), 'bar' => array(array())), $parentDefinition->getTags());
+
+ $container->compile();
+
+ $definition = $container->getDefinition(Bar::class);
+ $this->assertTrue($definition->isAutowired());
+ $this->assertTrue($definition->isLazy());
+ $this->assertEquals(array('foo' => array(array()), 'bar' => array(array())), $definition->getTags());
+ }
+}
+
+interface BarInterface
+{
+}
+
+class Bar implements BarInterface
+{
}
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
index 26937cd7168ee..7f5ee576bc9a3 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Tests\Loader;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
+use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
@@ -405,6 +406,27 @@ public function testDefaults()
$this->assertFalse($container->getDefinition('no_defaults_child')->isAutowired());
}
+ public function testInstanceof()
+ {
+ $container = new ContainerBuilder();
+ $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
+ $loader->load('services32.yml');
+
+ $definitions = $container->getDefinitions();
+ $parentDefinition = array_shift($definitions);
+ $this->assertNotInstanceOf(ChildDefinition::class, $parentDefinition);
+ $this->assertTrue($parentDefinition->isAutowired());
+ $this->assertTrue($parentDefinition->isLazy());
+ $this->assertEquals(array('foo' => array(array()), 'bar' => array(array())), $parentDefinition->getTags());
+
+ $container->compile();
+
+ $definition = $container->getDefinition(Foo::class);
+ $this->assertTrue($definition->isAutowired());
+ $this->assertTrue($definition->isLazy());
+ $this->assertEquals(array('foo' => array(array()), 'bar' => array(array())), $definition->getTags());
+ }
+
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* @expectedExceptionMessage The value of the "decorates" option for the "bar" service must be the id of the service without the "@" prefix (replace "@foo" with "foo").
@@ -415,3 +437,11 @@ public function testDecoratedServicesWithWrongSyntaxThrowsException()
$loader->load('bad_decorates.yml');
}
}
+
+interface FooInterface
+{
+}
+
+class Foo implements FooInterface
+{
+}