diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index c118ed3d2aa2e..b34002951bd31 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 3.3.0 ----- + * [EXPERIMENTAL] added "instanceof" section for local interface-defined configs * [EXPERIMENTAL] added "service-locator" argument for lazy loading a set of identified values and services * [EXPERIMENTAL] added prototype services for PSR4-based discovery and registration * added `ContainerBuilder::getReflectionClass()` for retrieving and tracking reflection class info diff --git a/src/Symfony/Component/DependencyInjection/ChildDefinition.php b/src/Symfony/Component/DependencyInjection/ChildDefinition.php index ba38a0b5cf698..58ff5762b4b81 100644 --- a/src/Symfony/Component/DependencyInjection/ChildDefinition.php +++ b/src/Symfony/Component/DependencyInjection/ChildDefinition.php @@ -119,6 +119,16 @@ public function setFile($file) return parent::setFile($file); } + /** + * {@inheritdoc} + */ + public function setShared($boolean) + { + $this->changes['shared'] = true; + + return parent::setShared($boolean); + } + /** * {@inheritdoc} */ @@ -139,6 +149,16 @@ public function setLazy($boolean) return parent::setLazy($boolean); } + /** + * {@inheritdoc} + */ + public function setAbstract($boolean) + { + $this->changes['abstract'] = true; + + return parent::setAbstract($boolean); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index 4be4345cd9e82..4c7422b35b9dc 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -42,6 +42,7 @@ public function __construct() $this->beforeOptimizationPasses = array( 100 => array( $resolveClassPass = new ResolveClassPass(), + new ResolveDefinitionInheritancePass(), ), ); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionInheritancePass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionInheritancePass.php new file mode 100644 index 0000000000000..19d06579d5174 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionInheritancePass.php @@ -0,0 +1,106 @@ + + * + * 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\ChildDefinition; +use Symfony\Component\DependencyInjection\Definition; + +/** + * Applies tags and instanceof inheritance to definitions. + * + * @author Nicolas Grekas
+ */
+class ResolveDefinitionInheritancePass extends AbstractRecursivePass
+{
+ protected function processValue($value, $isRoot = false)
+ {
+ if (!$value instanceof Definition) {
+ return parent::processValue($value, $isRoot);
+ }
+ if ($value instanceof ChildDefinition) {
+ $this->resolveDefinition($value);
+ }
+ $class = $value->getClass();
+ if (!$class || false !== strpos($class, '%') || !$instanceof = $value->getInstanceofConditionals()) {
+ return parent::processValue($value, $isRoot);
+ }
+
+ foreach ($instanceof as $interface => $definition) {
+ if ($interface !== $class && (!$this->container->getReflectionClass($interface) || !$this->container->getReflectionClass($class))) {
+ continue;
+ }
+ if ($interface === $class || is_subclass_of($class, $interface)) {
+ $this->mergeDefinition($value, $definition);
+ }
+ }
+
+ return parent::processValue($value, $isRoot);
+ }
+
+ /**
+ * Populates the class and tags from parent definitions.
+ */
+ private function resolveDefinition(ChildDefinition $definition)
+ {
+ if (!$this->container->has($parent = $definition->getParent())) {
+ return;
+ }
+
+ $parentDef = $this->container->findDefinition($parent);
+ if ($parentDef instanceof ChildDefinition) {
+ $this->resolveDefinition($parentDef);
+ }
+
+ if (!isset($definition->getChanges()['class'])) {
+ $definition->setClass($parentDef->getClass());
+ }
+
+ // append parent tags when inheriting is enabled
+ if ($definition->getInheritTags()) {
+ foreach ($parentDef->getTags() as $k => $v) {
+ foreach ($v as $v) {
+ $definition->addTag($k, $v);
+ }
+ }
+ }
+
+ $definition->setInheritTags(false);
+ }
+
+ private function mergeDefinition(Definition $def, ChildDefinition $definition)
+ {
+ $changes = $definition->getChanges();
+ if (isset($changes['shared'])) {
+ $def->setShared($definition->isShared());
+ }
+ if (isset($changes['abstract'])) {
+ $def->setAbstract($definition->isAbstract());
+ }
+ if (isset($changes['autowired_calls'])) {
+ $autowiredCalls = $def->getAutowiredCalls();
+ }
+
+ ResolveDefinitionTemplatesPass::mergeDefinition($def, $definition);
+
+ // merge autowired calls
+ if (isset($changes['autowired_calls'])) {
+ $def->setAutowiredCalls(array_merge($autowiredCalls, $def->getAutowiredCalls()));
+ }
+
+ // merge tags
+ foreach ($definition->getTags() as $k => $v) {
+ foreach ($v as $v) {
+ $def->addTag($k, $v);
+ }
+ }
+ }
+}
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php
index 375308835be5f..a1e6eb9533ef2 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php
@@ -103,6 +103,26 @@ private function doResolveDefinition(ChildDefinition $definition)
$def->setLazy($parentDef->isLazy());
$def->setAutowiredCalls($parentDef->getAutowiredCalls());
+ self::mergeDefinition($def, $definition);
+
+ // merge autowiring types
+ foreach ($definition->getAutowiringTypes(false) as $autowiringType) {
+ $def->addAutowiringType($autowiringType);
+ }
+
+ // these attributes are always taken from the child
+ $def->setAbstract($definition->isAbstract());
+ $def->setShared($definition->isShared());
+ $def->setTags($definition->getTags());
+
+ return $def;
+ }
+
+ /**
+ * @internal
+ */
+ public static function mergeDefinition(Definition $def, ChildDefinition $definition)
+ {
// overwrite with values specified in the decorator
$changes = $definition->getChanges();
if (isset($changes['class'])) {
@@ -168,26 +188,5 @@ private function doResolveDefinition(ChildDefinition $definition)
foreach ($definition->getOverriddenGetters() as $k => $v) {
$def->setOverriddenGetter($k, $v);
}
-
- // merge autowiring types
- foreach ($definition->getAutowiringTypes(false) as $autowiringType) {
- $def->addAutowiringType($autowiringType);
- }
-
- // these attributes are always taken from the child
- $def->setAbstract($definition->isAbstract());
- $def->setShared($definition->isShared());
- $def->setTags($definition->getTags());
-
- // append parent tags when inheriting is enabled
- if ($definition->getInheritTags()) {
- foreach ($parentDef->getTags() as $k => $v) {
- foreach ($v as $v) {
- $def->addTag($k, $v);
- }
- }
- }
-
- return $def;
}
}
diff --git a/src/Symfony/Component/DependencyInjection/Definition.php b/src/Symfony/Component/DependencyInjection/Definition.php
index 7cd781442ca67..17a294b4d2585 100644
--- a/src/Symfony/Component/DependencyInjection/Definition.php
+++ b/src/Symfony/Component/DependencyInjection/Definition.php
@@ -30,6 +30,7 @@ class Definition
private $properties = array();
private $calls = array();
private $getters = array();
+ private $instanceof = array();
private $configurator;
private $tags = array();
private $public = true;
@@ -363,6 +364,32 @@ public function getOverriddenGetters()
return $this->getters;
}
+ /**
+ * Sets the definition templates to conditionally apply on the current definition, keyed by parent interface/class.
+ *
+ * @param $instanceof ChildDefinition[]
+ *
+ * @experimental in version 3.3
+ */
+ public function setInstanceofConditionals(array $instanceof)
+ {
+ $this->instanceof = $instanceof;
+
+ return $this;
+ }
+
+ /**
+ * Gets the definition templates to conditionally apply on the current definition, keyed by parent interface/class.
+ *
+ * @return ChildDefinition[]
+ *
+ * @experimental in version 3.3
+ */
+ public function getInstanceofConditionals()
+ {
+ return $this->instanceof;
+ }
+
/**
* Sets tags for this definition.
*
@@ -736,9 +763,7 @@ public function getAutowiredCalls()
*/
public function setAutowired($autowired)
{
- $this->autowiredCalls = $autowired ? array('__construct') : array();
-
- return $this;
+ return $this->setAutowiredCalls($autowired ? array('__construct') : array());
}
/**
diff --git a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
index 68cf818c5bb47..ba0b77f0f5bf0 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
+use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
@@ -29,6 +30,8 @@
abstract class FileLoader extends BaseFileLoader
{
protected $container;
+ protected $isLoadingInstanceof = false;
+ protected $instanceof = array();
/**
* @param ContainerBuilder $container A ContainerBuilder instance
@@ -80,7 +83,22 @@ public function registerClasses(Definition $prototype, $namespace, $resource)
$prototype = serialize($prototype);
foreach ($classes as $class) {
- $this->container->setDefinition($class, unserialize($prototype));
+ $this->setDefinition($class, unserialize($prototype));
+ }
+ }
+
+ /**
+ * @experimental in version 3.3
+ */
+ protected function setDefinition($id, Definition $definition)
+ {
+ if ($this->isLoadingInstanceof) {
+ if (!$definition instanceof ChildDefinition) {
+ throw new InvalidArgumentException(sprintf('Invalid type definition "%s": ChildDefinition expected, "%s" given.', $id, get_class($definition)));
+ }
+ $this->instanceof[$id] = $definition;
+ } else {
+ $this->container->setDefinition($id, $definition->setInstanceofConditionals($this->instanceof));
}
}
diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
index 0902087a053c8..0809ab2630ad6 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
@@ -57,7 +57,11 @@ public function load($resource, $type = null)
$this->loadFromExtensions($xml);
// services
- $this->parseDefinitions($xml, $path);
+ try {
+ $this->parseDefinitions($xml, $path);
+ } finally {
+ $this->instanceof = array();
+ }
}
/**
@@ -126,13 +130,21 @@ private function parseDefinitions(\DOMDocument $xml, $file)
}
$this->setCurrentDir(dirname($file));
+ $this->instanceof = array();
+ $this->isLoadingInstanceof = true;
+ $instanceof = $xpath->query('//container:services/container:instanceof');
+ foreach ($instanceof as $service) {
+ $this->setDefinition((string) $service->getAttribute('id'), $this->parseDefinition($service, $file, array()));
+ }
+
+ $this->isLoadingInstanceof = false;
$defaults = $this->getServiceDefaults($xml, $file);
foreach ($services as $service) {
if (null !== $definition = $this->parseDefinition($service, $file, $defaults)) {
if ('prototype' === $service->tagName) {
$this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource'));
} else {
- $this->container->setDefinition((string) $service->getAttribute('id'), $definition);
+ $this->setDefinition((string) $service->getAttribute('id'), $definition);
}
}
}
@@ -209,7 +221,9 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults =
return;
}
- if ($parent = $service->getAttribute('parent')) {
+ if ($this->isLoadingInstanceof) {
+ $definition = new ChildDefinition('');
+ } elseif ($parent = $service->getAttribute('parent')) {
$definition = new ChildDefinition($parent);
if ($value = $service->getAttribute('inherit-tags')) {
@@ -247,7 +261,7 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults =
$definition->setDeprecated(true, $deprecated[0]->nodeValue ?: null);
}
- $definition->setArguments($this->getArgumentsAsPhp($service, 'argument', false, (bool) $parent));
+ $definition->setArguments($this->getArgumentsAsPhp($service, 'argument', false, $definition instanceof ChildDefinition));
$definition->setProperties($this->getArgumentsAsPhp($service, 'property'));
$definition->setOverriddenGetters($this->getArgumentsAsPhp($service, 'getter'));
@@ -422,7 +436,7 @@ private function processAnonymousServices(\DOMDocument $xml, $file)
uksort($definitions, 'strnatcmp');
foreach (array_reverse($definitions) as $id => list($domElement, $file, $wild)) {
if (null !== $definition = $this->parseDefinition($domElement, $file)) {
- $this->container->setDefinition($id, $definition);
+ $this->setDefinition($id, $definition);
}
if (true === $wild) {
diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
index e6b771faf2b4c..6ad27499e7b23 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
@@ -81,6 +81,22 @@ class YamlFileLoader extends FileLoader
'autowire' => 'autowire',
);
+ private static $instanceofKeywords = array(
+ 'shared' => 'shared',
+ 'lazy' => 'lazy',
+ 'public' => 'public',
+ 'abstract' => 'abstract',
+ 'deprecated' => 'deprecated',
+ 'factory' => 'factory',
+ 'arguments' => 'arguments',
+ 'properties' => 'properties',
+ 'getters' => 'getters',
+ 'configurator' => 'configurator',
+ 'calls' => 'calls',
+ 'tags' => 'tags',
+ 'autowire' => 'autowire',
+ );
+
private static $defaultsKeywords = array(
'public' => 'public',
'tags' => 'tags',
@@ -125,7 +141,11 @@ public function load($resource, $type = null)
// services
$this->setCurrentDir(dirname($path));
- $this->parseDefinitions($content, $resource);
+ try {
+ $this->parseDefinitions($content, $resource);
+ } finally {
+ $this->instanceof = array();
+ }
}
/**
@@ -187,6 +207,22 @@ private function parseDefinitions(array $content, $file)
throw new InvalidArgumentException(sprintf('The "services" key should contain an array in %s. Check your YAML syntax.', $file));
}
+ if ($this->isUnderscoredParamValid($content, '_instanceof', $file)) {
+ $this->instanceof = array();
+ $this->isLoadingInstanceof = true;
+ foreach ($content['services']['_instanceof'] as $id => $service) {
+ if (!$service || !is_array($service)) {
+ throw new InvalidArgumentException(sprintf('Type definition "%s" must be a non-empty array within "_instanceof" in %s. Check your YAML syntax.', $id, $file));
+ }
+ if (is_string($service) && 0 === strpos($service, '@')) {
+ throw new InvalidArgumentException(sprintf('Type definition "%s" cannot be an alias within "_instanceof" in %s. Check your YAML syntax.', $id, $file));
+ }
+ $this->parseDefinition($id, $service, $file, array());
+ }
+ unset($content['services']['_instanceof']);
+ }
+
+ $this->isLoadingInstanceof = false;
$defaults = $this->parseDefaults($content, $file);
foreach ($content['services'] as $id => $service) {
$this->parseDefinition($id, $service, $file, $defaults);
@@ -203,18 +239,11 @@ private function parseDefinitions(array $content, $file)
*/
private function parseDefaults(array &$content, $file)
{
- if (!isset($content['services']['_defaults'])) {
- return array();
- }
- 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 (isset($defaults['alias']) || isset($defaults['class']) || isset($defaults['factory'])) {
- // @deprecated code path, to be removed in 4.0
-
+ if (!$this->isUnderscoredParamValid($content, '_defaults', $file)) {
return array();
}
+ $defaults = $content['services']['_defaults'];
unset($content['services']['_defaults']);
foreach ($defaults as $key => $default) {
@@ -254,6 +283,21 @@ 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));
+ }
+
+ // @deprecated condition, to be removed in 4.0
+
+ return !isset($underscoreParam['alias']) && !isset($underscoreParam['class']) && !isset($underscoreParam['factory']);
+ }
+
/**
* @param array $service
*
@@ -304,7 +348,7 @@ private function parseDefinition($id, $service, $file, array $defaults)
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);
+ $this->checkDefinition($id, $service, $file);
if (isset($service['alias'])) {
$public = array_key_exists('public', $service) ? (bool) $service['public'] : (isset($defaults['public']) ? $defaults['public'] : true);
@@ -319,7 +363,9 @@ private function parseDefinition($id, $service, $file, array $defaults)
return;
}
- if (isset($service['parent'])) {
+ if ($this->isLoadingInstanceof) {
+ $definition = new ChildDefinition('');
+ } elseif (isset($service['parent'])) {
$definition = new ChildDefinition($service['parent']);
$inheritTag = isset($service['inherit_tags']) ? $service['inherit_tags'] : (isset($defaults['inherit_tags']) ? $defaults['inherit_tags'] : null);
@@ -494,7 +540,7 @@ private function parseDefinition($id, $service, $file, array $defaults)
}
$this->registerClasses($definition, $id, $service['resource']);
} else {
- $this->container->setDefinition($id, $definition);
+ $this->setDefinition($id, $definition);
}
}
@@ -723,12 +769,14 @@ private function loadFromExtensions(array $content)
* @param array $definition The service definition to check
* @param string $file The loaded YAML file
*/
- private static function checkDefinition($id, array $definition, $file)
+ private function checkDefinition($id, array $definition, $file)
{
- if ($throw = isset($definition['resource'])) {
- $keywords = static::$prototypeKeywords;
+ if ($throw = $this->isLoadingInstanceof) {
+ $keywords = self::$instanceofKeywords;
+ } elseif ($throw = isset($definition['resource'])) {
+ $keywords = self::$prototypeKeywords;
} else {
- $keywords = static::$serviceKeywords;
+ $keywords = self::$serviceKeywords;
}
foreach ($definition as $key => $value) {
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 1bdd2fc915ab1..37da2af19f15f 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
@@ -56,6 +56,7 @@