[DependencyInjection] Add "instanceof" section for local interface-defined configs

This commit is contained in:
Nicolas Grekas 2017-02-03 20:41:28 +01:00
parent 0a3cd973ae
commit 2fb601983f
10 changed files with 285 additions and 47 deletions

View File

@ -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

View File

@ -119,6 +119,16 @@ class ChildDefinition extends Definition
return parent::setFile($file);
}
/**
* {@inheritdoc}
*/
public function setShared($boolean)
{
$this->changes['shared'] = true;
return parent::setShared($boolean);
}
/**
* {@inheritdoc}
*/
@ -139,6 +149,16 @@ class ChildDefinition extends Definition
return parent::setLazy($boolean);
}
/**
* {@inheritdoc}
*/
public function setAbstract($boolean)
{
$this->changes['abstract'] = true;
return parent::setAbstract($boolean);
}
/**
* {@inheritdoc}
*/

View File

@ -42,6 +42,7 @@ class PassConfig
$this->beforeOptimizationPasses = array(
100 => array(
$resolveClassPass = new ResolveClassPass(),
new ResolveDefinitionInheritancePass(),
),
);

View File

@ -0,0 +1,106 @@
<?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\ChildDefinition;
use Symfony\Component\DependencyInjection\Definition;
/**
* Applies tags and instanceof inheritance to definitions.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
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);
}
}
}
}

View File

@ -103,6 +103,26 @@ class ResolveDefinitionTemplatesPass extends AbstractRecursivePass
$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 @@ class ResolveDefinitionTemplatesPass extends AbstractRecursivePass
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;
}
}

View File

@ -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 @@ class Definition
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 @@ class Definition
*/
public function setAutowired($autowired)
{
$this->autowiredCalls = $autowired ? array('__construct') : array();
return $this;
return $this->setAutowiredCalls($autowired ? array('__construct') : array());
}
/**

View File

@ -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 @@ use Symfony\Component\Finder\Glob;
abstract class FileLoader extends BaseFileLoader
{
protected $container;
protected $isLoadingInstanceof = false;
protected $instanceof = array();
/**
* @param ContainerBuilder $container A ContainerBuilder instance
@ -80,7 +83,22 @@ abstract class FileLoader extends BaseFileLoader
$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));
}
}

View File

@ -57,7 +57,11 @@ class XmlFileLoader extends FileLoader
$this->loadFromExtensions($xml);
// services
$this->parseDefinitions($xml, $path);
try {
$this->parseDefinitions($xml, $path);
} finally {
$this->instanceof = array();
}
}
/**
@ -126,13 +130,21 @@ class XmlFileLoader extends FileLoader
}
$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 @@ class XmlFileLoader extends FileLoader
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 @@ class XmlFileLoader extends FileLoader
$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 @@ class XmlFileLoader extends FileLoader
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) {

View File

@ -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 @@ class YamlFileLoader extends FileLoader
// services
$this->setCurrentDir(dirname($path));
$this->parseDefinitions($content, $resource);
try {
$this->parseDefinitions($content, $resource);
} finally {
$this->instanceof = array();
}
}
/**
@ -187,6 +207,22 @@ class YamlFileLoader extends FileLoader
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 @@ class YamlFileLoader extends FileLoader
*/
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) {
@ -304,7 +333,7 @@ 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.', 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 +348,9 @@ class YamlFileLoader extends FileLoader
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 +525,7 @@ class YamlFileLoader extends FileLoader
}
$this->registerClasses($definition, $id, $service['resource']);
} else {
$this->container->setDefinition($id, $definition);
$this->setDefinition($id, $definition);
}
}
@ -723,12 +754,14 @@ class YamlFileLoader extends FileLoader
* @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) {

View File

@ -56,6 +56,7 @@
<xsd:element name="service" type="service" minOccurs="1" />
<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:choice>
</xsd:complexType>
@ -137,6 +138,26 @@
<xsd:attribute name="inherit-tags" type="boolean" />
</xsd:complexType>
<xsd:complexType name="instanceof">
<xsd:choice maxOccurs="unbounded">
<xsd:element name="argument" type="argument" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="configurator" type="callable" minOccurs="0" maxOccurs="1" />
<xsd:element name="factory" type="callable" minOccurs="0" maxOccurs="1" />
<xsd:element name="deprecated" type="xsd:string" minOccurs="0" maxOccurs="1" />
<xsd:element name="call" type="call" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="tag" type="tag" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="property" type="property" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="getter" type="getter" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="autowire" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="id" type="xsd:string" use="required" />
<xsd:attribute name="shared" type="boolean" />
<xsd:attribute name="public" type="boolean" />
<xsd:attribute name="lazy" type="boolean" />
<xsd:attribute name="abstract" type="boolean" />
<xsd:attribute name="autowire" type="boolean" />
</xsd:complexType>
<xsd:complexType name="prototype">
<xsd:choice maxOccurs="unbounded">
<xsd:element name="argument" type="argument" minOccurs="0" maxOccurs="unbounded" />