feature #21327 [DI] Factorize compiler passes around new AbstractRecursivePass (nicolas-grekas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI] Factorize compiler passes around new AbstractRecursivePass

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

This PR introduces an AbstractRecursivePass that is able to visit the definition tree recursively everywhere a Definition or a Reference can be nested.

All existing compiler passes that need recursivity are updated to leverage this new class.
This remove a bunch of boilerplate that was previously repeated all over.
It also fixes compiler passes that eg missed looking at configurators for no reason (this "fix" is a new feature really).
It then applies recursivity to AutowirePass, that does not handle it today, but should.

I'm happy that the net result is a loss of 153 lines :)

Commits
-------

6acb80f48f [DI] Factorize compiler passes around new AbstractRecursivePass
This commit is contained in:
Fabien Potencier 2017-01-23 13:11:24 -08:00
commit 3f8aa7b220
11 changed files with 331 additions and 485 deletions

View File

@ -0,0 +1,76 @@
<?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\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
abstract class AbstractRecursivePass implements CompilerPassInterface
{
protected $container;
protected $currentId;
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$this->container = $container;
try {
$this->processValue($container->getDefinitions(), true);
} finally {
$this->container = null;
}
}
/**
* Processes a value found in a definition tree.
*
* @param mixed $value
* @param bool $isRoot
*
* @return mixed The processed value
*/
protected function processValue($value, $isRoot = false)
{
if (is_array($value)) {
foreach ($value as $k => $v) {
if ($isRoot) {
$this->currentId = $k;
}
if ($v !== $processedValue = $this->processValue($v, $isRoot)) {
$value[$k] = $processedValue;
}
}
} elseif ($value instanceof ArgumentInterface) {
$value->setValues($this->processValue($value->getValues()));
} elseif ($value instanceof Definition) {
$value->setArguments($this->processValue($value->getArguments()));
$value->setProperties($this->processValue($value->getProperties()));
$value->setMethodCalls($this->processValue($value->getMethodCalls()));
if ($v = $value->getFactory()) {
$value->setFactory($this->processValue($v));
}
if ($v = $value->getConfigurator()) {
$value->setConfigurator($this->processValue($v));
}
}
return $value;
}
}

View File

@ -25,14 +25,13 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
* *
* @author Johannes M. Schmitt <schmittjoh@gmail.com> * @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/ */
class AnalyzeServiceReferencesPass implements RepeatablePassInterface class AnalyzeServiceReferencesPass extends AbstractRecursivePass implements RepeatablePassInterface
{ {
private $graph; private $graph;
private $container;
private $currentId;
private $currentDefinition; private $currentDefinition;
private $repeatedPass; private $repeatedPass;
private $onlyConstructorArguments; private $onlyConstructorArguments;
private $lazy;
/** /**
* @param bool $onlyConstructorArguments Sets this Service Reference pass to ignore method calls * @param bool $onlyConstructorArguments Sets this Service Reference pass to ignore method calls
@ -60,68 +59,60 @@ class AnalyzeServiceReferencesPass implements RepeatablePassInterface
$this->container = $container; $this->container = $container;
$this->graph = $container->getCompiler()->getServiceReferenceGraph(); $this->graph = $container->getCompiler()->getServiceReferenceGraph();
$this->graph->clear(); $this->graph->clear();
$this->lazy = false;
foreach ($container->getDefinitions() as $id => $definition) {
if ($definition->isSynthetic() || $definition->isAbstract()) {
continue;
}
$this->currentId = $id;
$this->currentDefinition = $definition;
$this->processArguments($definition->getArguments());
if (is_array($definition->getFactory())) {
$this->processArguments($definition->getFactory());
}
if (!$this->onlyConstructorArguments) {
$this->processArguments($definition->getMethodCalls());
$this->processArguments($definition->getProperties());
if ($definition->getConfigurator()) {
$this->processArguments(array($definition->getConfigurator()));
}
}
}
foreach ($container->getAliases() as $id => $alias) { foreach ($container->getAliases() as $id => $alias) {
$this->graph->connect($id, $alias, (string) $alias, $this->getDefinition((string) $alias), null); $this->graph->connect($id, $alias, (string) $alias, $this->getDefinition((string) $alias), null);
} }
parent::process($container);
} }
/** protected function processValue($value, $isRoot = false)
* Processes service definitions for arguments to find relationships for the service graph.
*
* @param array $arguments An array of Reference or Definition objects relating to service definitions
* @param bool $lazy Whether the references nested in the arguments should be considered lazy or not
*/
private function processArguments(array $arguments, $lazy = false)
{ {
foreach ($arguments as $argument) { $lazy = $this->lazy;
if (is_array($argument)) {
$this->processArguments($argument, $lazy);
} elseif ($argument instanceof ArgumentInterface) {
$this->processArguments($argument->getValues(), true);
} elseif ($argument instanceof Reference) {
$targetDefinition = $this->getDefinition((string) $argument);
$this->graph->connect( if ($value instanceof ArgumentInterface) {
$this->currentId, $this->lazy = true;
$this->currentDefinition, parent::processValue($value);
$this->getDefinitionId((string) $argument), $this->lazy = $lazy;
$targetDefinition,
$argument,
$lazy || ($targetDefinition && $targetDefinition->isLazy())
);
} elseif ($argument instanceof Definition) {
$this->processArguments($argument->getArguments());
$this->processArguments($argument->getMethodCalls());
$this->processArguments($argument->getProperties());
if (is_array($argument->getFactory())) { return $value;
$this->processArguments($argument->getFactory());
}
}
} }
if ($value instanceof Reference) {
$targetDefinition = $this->getDefinition((string) $value);
$this->graph->connect(
$this->currentId,
$this->currentDefinition,
$this->getDefinitionId((string) $value),
$targetDefinition,
$value,
$this->lazy || ($targetDefinition && $targetDefinition->isLazy())
);
return $value;
}
if (!$value instanceof Definition) {
return parent::processValue($value, $isRoot);
}
if ($isRoot) {
if ($value->isSynthetic() || $value->isAbstract()) {
return $value;
}
$this->currentDefinition = $value;
}
$this->lazy = false;
if ($this->onlyConstructorArguments) {
$this->processValue($value->getFactory());
$this->processValue($value->getArguments());
} else {
parent::processValue($value, $isRoot);
}
$this->lazy = $lazy;
return $value;
} }
/** /**

View File

@ -22,12 +22,11 @@ use Symfony\Component\DependencyInjection\Reference;
* *
* @author Kévin Dunglas <dunglas@gmail.com> * @author Kévin Dunglas <dunglas@gmail.com>
*/ */
class AutowirePass implements CompilerPassInterface class AutowirePass extends AbstractRecursivePass implements CompilerPassInterface
{ {
/** /**
* @var ContainerBuilder * @var ContainerBuilder
*/ */
private $container;
private $reflectionClasses = array(); private $reflectionClasses = array();
private $definedTypes = array(); private $definedTypes = array();
private $types; private $types;
@ -42,17 +41,11 @@ class AutowirePass implements CompilerPassInterface
spl_autoload_register($throwingAutoloader); spl_autoload_register($throwingAutoloader);
try { try {
$this->container = $container; parent::process($container);
foreach ($container->getDefinitions() as $id => $definition) {
if ($autowiredMethods = $definition->getAutowiredMethods()) {
$this->completeDefinition($id, $definition, $autowiredMethods);
}
}
} finally { } finally {
spl_autoload_unregister($throwingAutoloader); spl_autoload_unregister($throwingAutoloader);
// Free memory and remove circular reference to container // Free memory and remove circular reference to container
$this->container = null;
$this->reflectionClasses = array(); $this->reflectionClasses = array();
$this->definedTypes = array(); $this->definedTypes = array();
$this->types = null; $this->types = null;
@ -85,18 +78,16 @@ class AutowirePass implements CompilerPassInterface
} }
/** /**
* Wires the given definition. * {@inheritdoc}
*
* @param string $id
* @param Definition $definition
* @param string[] $autowiredMethods
*
* @throws RuntimeException
*/ */
private function completeDefinition($id, Definition $definition, array $autowiredMethods) protected function processValue($value, $isRoot = false)
{ {
if (!$reflectionClass = $this->getReflectionClass($id, $definition)) { if (!$value instanceof Definition || !$autowiredMethods = $value->getAutowiredMethods()) {
return; return parent::processValue($value, $isRoot);
}
if (!$reflectionClass = $this->getReflectionClass($isRoot ? $this->currentId : null, $value)) {
return parent::processValue($value, $isRoot);
} }
if ($this->container->isTrackingResources()) { if ($this->container->isTrackingResources()) {
@ -104,27 +95,28 @@ class AutowirePass implements CompilerPassInterface
} }
$methodsCalled = array(); $methodsCalled = array();
foreach ($definition->getMethodCalls() as $methodCall) { foreach ($value->getMethodCalls() as $methodCall) {
$methodsCalled[$methodCall[0]] = true; $methodsCalled[strtolower($methodCall[0])] = true;
} }
foreach ($this->getMethodsToAutowire($id, $reflectionClass, $autowiredMethods) as $reflectionMethod) { foreach ($this->getMethodsToAutowire($reflectionClass, $autowiredMethods) as $reflectionMethod) {
if (!isset($methodsCalled[$reflectionMethod->name])) { if (!isset($methodsCalled[strtolower($reflectionMethod->name)])) {
$this->autowireMethod($id, $definition, $reflectionMethod); $this->autowireMethod($value, $reflectionMethod);
} }
} }
return parent::processValue($value, $isRoot);
} }
/** /**
* Gets the list of methods to autowire. * Gets the list of methods to autowire.
* *
* @param string $id
* @param \ReflectionClass $reflectionClass * @param \ReflectionClass $reflectionClass
* @param string[] $configuredAutowiredMethods * @param string[] $configuredAutowiredMethods
* *
* @return \ReflectionMethod[] * @return \ReflectionMethod[]
*/ */
private function getMethodsToAutowire($id, \ReflectionClass $reflectionClass, array $configuredAutowiredMethods) private function getMethodsToAutowire(\ReflectionClass $reflectionClass, array $configuredAutowiredMethods)
{ {
$found = array(); $found = array();
$regexList = array(); $regexList = array();
@ -157,20 +149,19 @@ class AutowirePass implements CompilerPassInterface
if ($notFound = array_diff($configuredAutowiredMethods, $found)) { if ($notFound = array_diff($configuredAutowiredMethods, $found)) {
$compiler = $this->container->getCompiler(); $compiler = $this->container->getCompiler();
$compiler->addLogMessage($compiler->getLoggingFormatter()->formatUnusedAutowiringPatterns($this, $id, $notFound)); $compiler->addLogMessage($compiler->getLoggingFormatter()->formatUnusedAutowiringPatterns($this, $this->currentId, $notFound));
} }
} }
/** /**
* Autowires the constructor or a setter. * Autowires the constructor or a setter.
* *
* @param string $id
* @param Definition $definition * @param Definition $definition
* @param \ReflectionMethod $reflectionMethod * @param \ReflectionMethod $reflectionMethod
* *
* @throws RuntimeException * @throws RuntimeException
*/ */
private function autowireMethod($id, Definition $definition, \ReflectionMethod $reflectionMethod) private function autowireMethod(Definition $definition, \ReflectionMethod $reflectionMethod)
{ {
if ($isConstructor = $reflectionMethod->isConstructor()) { if ($isConstructor = $reflectionMethod->isConstructor()) {
$arguments = $definition->getArguments(); $arguments = $definition->getArguments();
@ -189,7 +180,7 @@ class AutowirePass implements CompilerPassInterface
// no default value? Then fail // no default value? Then fail
if (!$parameter->isOptional()) { if (!$parameter->isOptional()) {
if ($isConstructor) { if ($isConstructor) {
throw new RuntimeException(sprintf('Unable to autowire argument index %d ($%s) for the service "%s". If this is an object, give it a type-hint. Otherwise, specify this argument\'s value explicitly.', $index, $parameter->name, $id)); throw new RuntimeException(sprintf('Unable to autowire argument index %d ($%s) for the service "%s". If this is an object, give it a type-hint. Otherwise, specify this argument\'s value explicitly.', $index, $parameter->name, $this->currentId));
} }
return; return;
@ -210,7 +201,7 @@ class AutowirePass implements CompilerPassInterface
$addMethodCall = true; $addMethodCall = true;
} else { } else {
try { try {
$value = $this->createAutowiredDefinition($typeHint, $id); $value = $this->createAutowiredDefinition($typeHint);
$addMethodCall = true; $addMethodCall = true;
} catch (RuntimeException $e) { } catch (RuntimeException $e) {
if ($parameter->allowsNull()) { if ($parameter->allowsNull()) {
@ -338,38 +329,40 @@ class AutowirePass implements CompilerPassInterface
* Registers a definition for the type if possible or throws an exception. * Registers a definition for the type if possible or throws an exception.
* *
* @param \ReflectionClass $typeHint * @param \ReflectionClass $typeHint
* @param string $id
* *
* @return Reference A reference to the registered definition * @return Reference A reference to the registered definition
* *
* @throws RuntimeException * @throws RuntimeException
*/ */
private function createAutowiredDefinition(\ReflectionClass $typeHint, $id) private function createAutowiredDefinition(\ReflectionClass $typeHint)
{ {
if (isset($this->ambiguousServiceTypes[$typeHint->name])) { if (isset($this->ambiguousServiceTypes[$typeHint->name])) {
$classOrInterface = $typeHint->isInterface() ? 'interface' : 'class'; $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class';
$matchingServices = implode(', ', $this->ambiguousServiceTypes[$typeHint->name]); $matchingServices = implode(', ', $this->ambiguousServiceTypes[$typeHint->name]);
throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". Multiple services exist for this %s (%s).', $typeHint->name, $id, $classOrInterface, $matchingServices), 1); throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". Multiple services exist for this %s (%s).', $typeHint->name, $this->currentId, $classOrInterface, $matchingServices), 1);
} }
if (!$typeHint->isInstantiable()) { if (!$typeHint->isInstantiable()) {
$classOrInterface = $typeHint->isInterface() ? 'interface' : 'class'; $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class';
throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". No services were found matching this %s and it cannot be auto-registered.', $typeHint->name, $id, $classOrInterface)); throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". No services were found matching this %s and it cannot be auto-registered.', $typeHint->name, $this->currentId, $classOrInterface));
} }
$argumentId = sprintf('autowired.%s', $typeHint->name); $currentId = $this->currentId;
$this->currentId = $argumentId = sprintf('autowired.%s', $typeHint->name);
$argumentDefinition = $this->container->register($argumentId, $typeHint->name); $argumentDefinition = $this->container->register($argumentId, $typeHint->name);
$argumentDefinition->setPublic(false); $argumentDefinition->setPublic(false);
$argumentDefinition->setAutowired(true);
$this->populateAvailableType($argumentId, $argumentDefinition); $this->populateAvailableType($argumentId, $argumentDefinition);
try { try {
$this->completeDefinition($argumentId, $argumentDefinition, array('__construct')); $this->processValue($argumentDefinition, true);
$this->currentId = $currentId;
} catch (RuntimeException $e) { } catch (RuntimeException $e) {
$classOrInterface = $typeHint->isInterface() ? 'interface' : 'class'; $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class';
$message = sprintf('Unable to autowire argument of type "%s" for the service "%s". No services were found matching this %s and it cannot be auto-registered.', $typeHint->name, $id, $classOrInterface); $message = sprintf('Unable to autowire argument of type "%s" for the service "%s". No services were found matching this %s and it cannot be auto-registered.', $typeHint->name, $this->currentId, $classOrInterface);
throw new RuntimeException($message, 0, $e); throw new RuntimeException($message, 0, $e);
} }
@ -379,14 +372,14 @@ class AutowirePass implements CompilerPassInterface
/** /**
* Retrieves the reflection class associated with the given service. * Retrieves the reflection class associated with the given service.
* *
* @param string $id * @param string|null $id
* @param Definition $definition * @param Definition $definition
* *
* @return \ReflectionClass|false * @return \ReflectionClass|false
*/ */
private function getReflectionClass($id, Definition $definition) private function getReflectionClass($id, Definition $definition)
{ {
if (isset($this->reflectionClasses[$id])) { if (null !== $id && isset($this->reflectionClasses[$id])) {
return $this->reflectionClasses[$id]; return $this->reflectionClasses[$id];
} }
@ -403,7 +396,11 @@ class AutowirePass implements CompilerPassInterface
$reflector = false; $reflector = false;
} }
return $this->reflectionClasses[$id] = $reflector; if (null !== $id) {
$this->reflectionClasses[$id] = $reflector;
}
return $reflector;
} }
private function addServiceToAmbiguousType($id, $type) private function addServiceToAmbiguousType($id, $type)

View File

@ -11,56 +11,27 @@
namespace Symfony\Component\DependencyInjection\Compiler; namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/** /**
* Checks that all references are pointing to a valid service. * Checks that all references are pointing to a valid service.
* *
* @author Johannes M. Schmitt <schmittjoh@gmail.com> * @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/ */
class CheckExceptionOnInvalidReferenceBehaviorPass implements CompilerPassInterface class CheckExceptionOnInvalidReferenceBehaviorPass extends AbstractRecursivePass implements CompilerPassInterface
{ {
private $container; protected function processValue($value, $isRoot = false)
private $sourceId;
public function process(ContainerBuilder $container)
{ {
$this->container = $container; if ($value instanceof Reference && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()) {
$destId = (string) $value;
foreach ($container->getDefinitions() as $id => $definition) { if (!$this->container->has($destId)) {
$this->sourceId = $id; throw new ServiceNotFoundException($destId, $this->currentId);
$this->processDefinition($definition);
}
}
private function processDefinition(Definition $definition)
{
$this->processReferences($definition->getArguments());
$this->processReferences($definition->getMethodCalls());
$this->processReferences($definition->getProperties());
}
private function processReferences(array $arguments)
{
foreach ($arguments as $argument) {
if (is_array($argument)) {
$this->processReferences($argument);
} elseif ($argument instanceof ArgumentInterface) {
$this->processReferences($argument->getValues());
} elseif ($argument instanceof Definition) {
$this->processDefinition($argument);
} elseif ($argument instanceof Reference && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE === $argument->getInvalidBehavior()) {
$destId = (string) $argument;
if (!$this->container->has($destId)) {
throw new ServiceNotFoundException($destId, $this->sourceId);
}
} }
} }
return parent::processValue($value, $isRoot);
} }
} }

View File

@ -11,10 +11,8 @@
namespace Symfony\Component\DependencyInjection\Compiler; namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Exception\RuntimeException;
/** /**
@ -25,75 +23,26 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException;
* *
* @author Johannes M. Schmitt <schmittjoh@gmail.com> * @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/ */
class CheckReferenceValidityPass implements CompilerPassInterface class CheckReferenceValidityPass extends AbstractRecursivePass implements CompilerPassInterface
{ {
private $container; protected function processValue($value, $isRoot = false)
private $currentId;
/**
* Processes the ContainerBuilder to validate References.
*
* @param ContainerBuilder $container
*/
public function process(ContainerBuilder $container)
{ {
$this->container = $container; if ($isRoot && $value instanceof Definition && ($value->isSynthetic() || $value->isAbstract())) {
return $value;
foreach ($container->getDefinitions() as $id => $definition) {
if ($definition->isSynthetic() || $definition->isAbstract()) {
continue;
}
$this->currentId = $id;
$this->validateReferences($definition->getArguments());
$this->validateReferences($definition->getMethodCalls());
$this->validateReferences($definition->getProperties());
} }
} if ($value instanceof Reference && $this->container->hasDefinition((string) $value)) {
$targetDefinition = $this->container->getDefinition((string) $value);
/** if ($targetDefinition->isAbstract()) {
* Validates an array of References. throw new RuntimeException(sprintf(
* 'The definition "%s" has a reference to an abstract definition "%s". '
* @param array $arguments An array of Reference objects .'Abstract definitions cannot be the target of references.',
* $this->currentId,
* @throws RuntimeException when there is a reference to an abstract definition. $value
*/ ));
private function validateReferences(array $arguments)
{
foreach ($arguments as $argument) {
if (is_array($argument)) {
$this->validateReferences($argument);
} elseif ($argument instanceof ArgumentInterface) {
$this->validateReferences($argument->getValues());
} elseif ($argument instanceof Reference) {
$targetDefinition = $this->getDefinition((string) $argument);
if (null !== $targetDefinition && $targetDefinition->isAbstract()) {
throw new RuntimeException(sprintf(
'The definition "%s" has a reference to an abstract definition "%s". '
.'Abstract definitions cannot be the target of references.',
$this->currentId,
$argument
));
}
} }
} }
}
/** return parent::processValue($value, $isRoot);
* Returns the Definition given an id.
*
* @param string $id Definition identifier
*
* @return Definition
*/
private function getDefinition($id)
{
if (!$this->container->hasDefinition($id)) {
return;
}
return $this->container->getDefinition($id);
} }
} }

View File

@ -14,20 +14,15 @@ namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/** /**
* Inline service definitions where this is possible. * Inline service definitions where this is possible.
* *
* @author Johannes M. Schmitt <schmittjoh@gmail.com> * @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/ */
class InlineServiceDefinitionsPass implements RepeatablePassInterface class InlineServiceDefinitionsPass extends AbstractRecursivePass implements RepeatablePassInterface
{ {
private $repeatedPass; private $repeatedPass;
private $graph;
private $compiler;
private $formatter;
private $currentId;
/** /**
* {@inheritdoc} * {@inheritdoc}
@ -38,77 +33,38 @@ class InlineServiceDefinitionsPass implements RepeatablePassInterface
} }
/** /**
* Processes the ContainerBuilder for inline service definitions. * {@inheritdoc}
*
* @param ContainerBuilder $container
*/ */
public function process(ContainerBuilder $container) protected function processValue($value, $isRoot = false)
{ {
$this->compiler = $container->getCompiler(); if ($value instanceof ArgumentInterface) {
$this->formatter = $this->compiler->getLoggingFormatter(); $this->processValue($value->getValues());
$this->graph = $this->compiler->getServiceReferenceGraph();
$container->setDefinitions($this->inlineArguments($container, $container->getDefinitions(), true)); return $value;
} }
if ($value instanceof Reference && $this->container->hasDefinition($id = (string) $value)) {
$compiler = $this->container->getCompiler();
$definition = $this->container->getDefinition($id);
/** if ($this->isInlineableDefinition($id, $definition, $compiler->getServiceReferenceGraph())) {
* Processes inline arguments. $compiler->addLogMessage($compiler->getLoggingFormatter()->formatInlineService($this, $id, $this->currentId));
*
* @param ContainerBuilder $container The ContainerBuilder if ($definition->isShared()) {
* @param array $arguments An array of arguments return $definition;
* @param bool $isRoot If we are processing the root definitions or not
*
* @return array
*/
private function inlineArguments(ContainerBuilder $container, array $arguments, $isRoot = false)
{
foreach ($arguments as $k => $argument) {
if ($isRoot) {
$this->currentId = $k;
}
if (is_array($argument)) {
$arguments[$k] = $this->inlineArguments($container, $argument);
} elseif ($argument instanceof ArgumentInterface) {
$this->inlineArguments($container, $argument->getValues());
} elseif ($argument instanceof Reference) {
if (!$container->hasDefinition($id = (string) $argument)) {
continue;
} }
$value = clone $definition;
if ($this->isInlineableDefinition($id, $definition = $container->getDefinition($id))) {
$this->compiler->addLogMessage($this->formatter->formatInlineService($this, $id, $this->currentId));
if ($definition->isShared()) {
$arguments[$k] = $definition;
} else {
$arguments[$k] = clone $definition;
}
}
} elseif ($argument instanceof Definition) {
$argument->setArguments($this->inlineArguments($container, $argument->getArguments()));
$argument->setMethodCalls($this->inlineArguments($container, $argument->getMethodCalls()));
$argument->setProperties($this->inlineArguments($container, $argument->getProperties()));
$configurator = $this->inlineArguments($container, array($argument->getConfigurator()));
$argument->setConfigurator($configurator[0]);
$factory = $this->inlineArguments($container, array($argument->getFactory()));
$argument->setFactory($factory[0]);
} }
} }
return $arguments; return parent::processValue($value, $isRoot);
} }
/** /**
* Checks if the definition is inlineable. * Checks if the definition is inlineable.
* *
* @param string $id
* @param Definition $definition
*
* @return bool If the definition is inlineable * @return bool If the definition is inlineable
*/ */
private function isInlineableDefinition($id, Definition $definition) private function isInlineableDefinition($id, Definition $definition, ServiceReferenceGraph $graph)
{ {
if (!$definition->isShared()) { if (!$definition->isShared()) {
return true; return true;
@ -118,7 +74,7 @@ class InlineServiceDefinitionsPass implements RepeatablePassInterface
return false; return false;
} }
if (!$this->graph->hasNode($id)) { if (!$graph->hasNode($id)) {
return true; return true;
} }
@ -127,7 +83,7 @@ class InlineServiceDefinitionsPass implements RepeatablePassInterface
} }
$ids = array(); $ids = array();
foreach ($this->graph->getNode($id)->getInEdges() as $edge) { foreach ($graph->getNode($id)->getInEdges() as $edge) {
$ids[] = $edge->getSourceNode()->getId(); $ids[] = $edge->getSourceNode()->getId();
} }

View File

@ -11,7 +11,6 @@
namespace Symfony\Component\DependencyInjection\Compiler; namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
@ -22,10 +21,9 @@ use Symfony\Component\DependencyInjection\Reference;
* *
* @author Johannes M. Schmitt <schmittjoh@gmail.com> * @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/ */
class ReplaceAliasByActualDefinitionPass implements CompilerPassInterface class ReplaceAliasByActualDefinitionPass extends AbstractRecursivePass implements CompilerPassInterface
{ {
private $compiler; private $replacements;
private $formatter;
/** /**
* Process the Container to replace aliases with service definitions. * Process the Container to replace aliases with service definitions.
@ -36,9 +34,6 @@ class ReplaceAliasByActualDefinitionPass implements CompilerPassInterface
*/ */
public function process(ContainerBuilder $container) public function process(ContainerBuilder $container)
{ {
// Setup
$this->compiler = $container->getCompiler();
$this->formatter = $this->compiler->getLoggingFormatter();
// First collect all alias targets that need to be replaced // First collect all alias targets that need to be replaced
$seenAliasTargets = array(); $seenAliasTargets = array();
$replacements = array(); $replacements = array();
@ -72,60 +67,25 @@ class ReplaceAliasByActualDefinitionPass implements CompilerPassInterface
$container->removeDefinition($targetId); $container->removeDefinition($targetId);
$replacements[$targetId] = $definitionId; $replacements[$targetId] = $definitionId;
} }
$this->replacements = $replacements;
// Now replace target instances in all definitions parent::process($container);
foreach ($container->getDefinitions() as $definitionId => $definition) { $this->replacements = array();
$definition->setArguments($this->updateArgumentReferences($replacements, $definitionId, $definition->getArguments()));
$definition->setMethodCalls($this->updateArgumentReferences($replacements, $definitionId, $definition->getMethodCalls()));
$definition->setProperties($this->updateArgumentReferences($replacements, $definitionId, $definition->getProperties()));
$definition->setFactory($this->updateFactoryReference($replacements, $definition->getFactory()));
}
} }
/** /**
* Recursively updates references in an array. * {@inheritdoc}
*
* @param array $replacements Table of aliases to replace
* @param string $definitionId Identifier of this definition
* @param array $arguments Where to replace the aliases
*
* @return array
*/ */
private function updateArgumentReferences(array $replacements, $definitionId, array $arguments) protected function processValue($value, $isRoot = false)
{ {
foreach ($arguments as $k => $argument) { if ($value instanceof Reference && isset($this->replacements[$referenceId = (string) $value])) {
// Handle recursion step
if (is_array($argument)) {
$arguments[$k] = $this->updateArgumentReferences($replacements, $definitionId, $argument);
continue;
}
if ($argument instanceof ArgumentInterface) {
$argument->setValues($this->updateArgumentReferences($replacements, $definitionId, $argument->getValues()));
continue;
}
// Skip arguments that don't need replacement
if (!$argument instanceof Reference) {
continue;
}
$referenceId = (string) $argument;
if (!isset($replacements[$referenceId])) {
continue;
}
// Perform the replacement // Perform the replacement
$newId = $replacements[$referenceId]; $newId = $this->replacements[$referenceId];
$arguments[$k] = new Reference($newId, $argument->getInvalidBehavior()); $value = new Reference($newId, $value->getInvalidBehavior());
$this->compiler->addLogMessage($this->formatter->formatUpdateReference($this, $definitionId, $referenceId, $newId)); $compiler = $this->container->getCompiler();
$compiler->addLogMessage($compiler->getLoggingFormatter()->formatUpdateReference($this, $this->currentId, $referenceId, $newId));
} }
return $arguments; return parent::processValue($value, $isRoot);
}
private function updateFactoryReference(array $replacements, $factory)
{
if (is_array($factory) && $factory[0] instanceof Reference && isset($replacements[$referenceId = (string) $factory[0]])) {
$factory[0] = new Reference($replacements[$referenceId], $factory[0]->getInvalidBehavior());
}
return $factory;
} }
} }

View File

@ -11,10 +11,8 @@
namespace Symfony\Component\DependencyInjection\Compiler; namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\ExceptionInterface; use Symfony\Component\DependencyInjection\Exception\ExceptionInterface;
use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Exception\RuntimeException;
@ -25,83 +23,39 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException;
* @author Johannes M. Schmitt <schmittjoh@gmail.com> * @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @author Nicolas Grekas <p@tchwork.com> * @author Nicolas Grekas <p@tchwork.com>
*/ */
class ResolveDefinitionTemplatesPass implements CompilerPassInterface class ResolveDefinitionTemplatesPass extends AbstractRecursivePass implements CompilerPassInterface
{ {
private $compiler; protected function processValue($value, $isRoot = false)
private $formatter;
private $currentId;
/**
* Process the ContainerBuilder to replace ChildDefinition instances with their real Definition instances.
*
* @param ContainerBuilder $container
*/
public function process(ContainerBuilder $container)
{ {
$this->compiler = $container->getCompiler(); if (!$value instanceof Definition) {
$this->formatter = $this->compiler->getLoggingFormatter(); return parent::processValue($value, $isRoot);
}
$container->setDefinitions($this->resolveArguments($container, $container->getDefinitions(), true)); if ($isRoot) {
} // yes, we are specifically fetching the definition from the
// container to ensure we are not operating on stale data
/** $value = $this->container->getDefinition($this->currentId);
* Resolves definition decorator arguments. }
* if ($value instanceof ChildDefinition) {
* @param ContainerBuilder $container The ContainerBuilder $value = $this->resolveDefinition($value);
* @param array $arguments An array of arguments
* @param bool $isRoot If we are processing the root definitions or not
*
* @return array
*/
private function resolveArguments(ContainerBuilder $container, array $arguments, $isRoot = false)
{
foreach ($arguments as $k => $argument) {
if ($isRoot) { if ($isRoot) {
// yes, we are specifically fetching the definition from the $this->container->setDefinition($this->currentId, $value);
// container to ensure we are not operating on stale data
$arguments[$k] = $argument = $container->getDefinition($k);
$this->currentId = $k;
}
if (is_array($argument)) {
$arguments[$k] = $this->resolveArguments($container, $argument);
} elseif ($argument instanceof ArgumentInterface) {
$argument->setValues($this->resolveArguments($container, $argument->getValues()));
} elseif ($argument instanceof Definition) {
if ($argument instanceof ChildDefinition) {
$arguments[$k] = $argument = $this->resolveDefinition($container, $argument);
if ($isRoot) {
$container->setDefinition($k, $argument);
}
}
$argument->setArguments($this->resolveArguments($container, $argument->getArguments()));
$argument->setMethodCalls($this->resolveArguments($container, $argument->getMethodCalls()));
$argument->setProperties($this->resolveArguments($container, $argument->getProperties()));
$configurator = $this->resolveArguments($container, array($argument->getConfigurator()));
$argument->setConfigurator($configurator[0]);
$factory = $this->resolveArguments($container, array($argument->getFactory()));
$argument->setFactory($factory[0]);
} }
} }
return $arguments; return parent::processValue($value, $isRoot);
} }
/** /**
* Resolves the definition. * Resolves the definition.
* *
* @param ContainerBuilder $container The ContainerBuilder
* @param ChildDefinition $definition
*
* @return Definition * @return Definition
* *
* @throws \RuntimeException When the definition is invalid * @throws RuntimeException When the definition is invalid
*/ */
private function resolveDefinition(ContainerBuilder $container, ChildDefinition $definition) private function resolveDefinition(ChildDefinition $definition)
{ {
try { try {
return $this->doResolveDefinition($container, $definition); return $this->doResolveDefinition($definition);
} catch (ExceptionInterface $e) { } catch (ExceptionInterface $e) {
$r = new \ReflectionProperty($e, 'message'); $r = new \ReflectionProperty($e, 'message');
$r->setAccessible(true); $r->setAccessible(true);
@ -111,22 +65,23 @@ class ResolveDefinitionTemplatesPass implements CompilerPassInterface
} }
} }
private function doResolveDefinition(ContainerBuilder $container, ChildDefinition $definition) private function doResolveDefinition(ChildDefinition $definition)
{ {
if (!$container->has($parent = $definition->getParent())) { if (!$this->container->has($parent = $definition->getParent())) {
throw new RuntimeException(sprintf('Parent definition "%s" does not exist.', $parent)); throw new RuntimeException(sprintf('Parent definition "%s" does not exist.', $parent));
} }
$parentDef = $container->findDefinition($parent); $parentDef = $this->container->findDefinition($parent);
if ($parentDef instanceof ChildDefinition) { if ($parentDef instanceof ChildDefinition) {
$id = $this->currentId; $id = $this->currentId;
$this->currentId = $parent; $this->currentId = $parent;
$parentDef = $this->resolveDefinition($container, $parentDef); $parentDef = $this->resolveDefinition($parentDef);
$container->setDefinition($parent, $parentDef); $this->container->setDefinition($parent, $parentDef);
$this->currentId = $id; $this->currentId = $id;
} }
$this->compiler->addLogMessage($this->formatter->formatResolveInheritance($this, $this->currentId, $parent)); $compiler = $this->container->getCompiler();
$compiler->addLogMessage($compiler->getLoggingFormatter()->formatResolveInheritance($this, $this->currentId, $parent));
$def = new Definition(); $def = new Definition();
// merge in parent definition // merge in parent definition

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Exception\RuntimeException;
@ -26,6 +27,7 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException;
class ResolveInvalidReferencesPass implements CompilerPassInterface class ResolveInvalidReferencesPass implements CompilerPassInterface
{ {
private $container; private $container;
private $signalingException;
/** /**
* Process the ContainerBuilder to resolve invalid references. * Process the ContainerBuilder to resolve invalid references.
@ -35,86 +37,76 @@ class ResolveInvalidReferencesPass implements CompilerPassInterface
public function process(ContainerBuilder $container) public function process(ContainerBuilder $container)
{ {
$this->container = $container; $this->container = $container;
foreach ($container->getDefinitions() as $definition) { $this->signalingException = new RuntimeException('Invalid reference.');
if ($definition->isSynthetic() || $definition->isAbstract()) {
continue;
}
$definition->setArguments( try {
$this->processArguments($definition->getArguments()) $this->processValue($container->getDefinitions(), 1);
); } finally {
$this->container = $this->signalingException = null;
$calls = array();
foreach ($definition->getMethodCalls() as $call) {
try {
$calls[] = array($call[0], $this->processArguments($call[1], true));
} catch (RuntimeException $e) {
// this call is simply removed
}
}
$definition->setMethodCalls($calls);
$properties = array();
foreach ($definition->getProperties() as $name => $value) {
try {
$value = $this->processArguments(array($value), true);
$properties[$name] = reset($value);
} catch (RuntimeException $e) {
// ignore property
}
}
$definition->setProperties($properties);
} }
} }
/** /**
* Processes arguments to determine invalid references. * Processes arguments to determine invalid references.
* *
* @param array $arguments An array of Reference objects * @throws RuntimeException When an invalid reference is found
* @param bool $inMethodCall
* @param bool $inCollection
*
* @return array
*
* @throws RuntimeException When the config is invalid
*/ */
private function processArguments(array $arguments, $inMethodCall = false, $inCollection = false) private function processValue($value, $rootLevel = 0, $level = 0)
{ {
$isNumeric = array_keys($arguments) === range(0, count($arguments) - 1); if ($value instanceof Definition) {
if ($value->isSynthetic() || $value->isAbstract()) {
return $value;
}
$value->setArguments($this->processValue($value->getArguments(), 0));
$value->setProperties($this->processValue($value->getProperties(), 1));
$value->setMethodCalls($this->processValue($value->getMethodCalls(), 2));
} elseif (is_array($value)) {
$i = 0;
foreach ($arguments as $k => $argument) { foreach ($value as $k => $v) {
if (is_array($argument)) { try {
$arguments[$k] = $this->processArguments($argument, $inMethodCall, true); if (false !== $i && $k !== $i++) {
} elseif ($argument instanceof ArgumentInterface) { $i = false;
$argument->setValues($this->processArguments($argument->getValues(), $inMethodCall, true));
} elseif ($argument instanceof Reference) {
$id = (string) $argument;
$invalidBehavior = $argument->getInvalidBehavior();
$exists = $this->container->has($id);
// resolve invalid behavior
if (!$exists && ContainerInterface::NULL_ON_INVALID_REFERENCE === $invalidBehavior) {
$arguments[$k] = null;
} elseif (!$exists && ContainerInterface::IGNORE_ON_INVALID_REFERENCE === $invalidBehavior) {
if ($inCollection) {
unset($arguments[$k]);
continue;
} }
if ($inMethodCall) { if ($v !== $processedValue = $this->processValue($v, $rootLevel, 1 + $level)) {
throw new RuntimeException('Method shouldn\'t be called.'); $value[$k] = $processedValue;
}
} catch (RuntimeException $e) {
if ($rootLevel < $level || ($rootLevel && !$level)) {
unset($value[$k]);
} elseif ($rootLevel) {
throw $e;
} else {
$value[$k] = null;
} }
$arguments[$k] = null;
} }
} }
// Ensure numerically indexed arguments have sequential numeric keys.
if (false !== $i) {
$value = array_values($value);
}
} elseif ($value instanceof ArgumentInterface) {
$value->setValues($this->processValue($value->getValues(), $rootLevel, 1 + $level));
} elseif ($value instanceof Reference) {
$id = (string) $value;
if ($this->container->has($id)) {
return $value;
}
$invalidBehavior = $value->getInvalidBehavior();
// resolve invalid behavior
if (ContainerInterface::NULL_ON_INVALID_REFERENCE === $invalidBehavior) {
$value = null;
} elseif (ContainerInterface::IGNORE_ON_INVALID_REFERENCE === $invalidBehavior) {
if (0 < $level || $rootLevel) {
throw $this->signalingException;
}
$value = null;
}
} }
// Ensure numerically indexed arguments have sequential numeric keys. return $value;
if ($isNumeric) {
$arguments = array_values($arguments);
}
return $arguments;
} }
} }

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Compiler; namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException; use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
/** /**
@ -19,52 +20,50 @@ use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
* *
* @author Johannes M. Schmitt <schmittjoh@gmail.com> * @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/ */
class ResolveParameterPlaceHoldersPass implements CompilerPassInterface class ResolveParameterPlaceHoldersPass extends AbstractRecursivePass implements CompilerPassInterface
{ {
private $bag;
/** /**
* Processes the ContainerBuilder to resolve parameter placeholders. * {@inheritdoc}
*
* @param ContainerBuilder $container
* *
* @throws ParameterNotFoundException * @throws ParameterNotFoundException
*/ */
public function process(ContainerBuilder $container) public function process(ContainerBuilder $container)
{ {
$parameterBag = $container->getParameterBag(); $this->bag = $container->getParameterBag();
foreach ($container->getDefinitions() as $id => $definition) { try {
try { parent::process($container);
$definition->setClass($parameterBag->resolveValue($definition->getClass()));
$definition->setFile($parameterBag->resolveValue($definition->getFile()));
$definition->setArguments($parameterBag->resolveValue($definition->getArguments()));
$factory = $definition->getFactory(); $aliases = array();
foreach ($container->getAliases() as $name => $target) {
if (is_array($factory) && isset($factory[0])) { $this->currentId = $name;
$factory[0] = $parameterBag->resolveValue($factory[0]); $aliases[$this->bag->resolveValue($name)] = $this->bag->resolveValue($target);
$definition->setFactory($factory);
}
$calls = array();
foreach ($definition->getMethodCalls() as $name => $arguments) {
$calls[$parameterBag->resolveValue($name)] = $parameterBag->resolveValue($arguments);
}
$definition->setMethodCalls($calls);
$definition->setProperties($parameterBag->resolveValue($definition->getProperties()));
} catch (ParameterNotFoundException $e) {
$e->setSourceId($id);
throw $e;
} }
$container->setAliases($aliases);
} catch (ParameterNotFoundException $e) {
$e->setSourceId($this->currentId);
throw $e;
} }
$aliases = array(); $this->bag->resolve();
foreach ($container->getAliases() as $name => $target) { $this->bag = null;
$aliases[$parameterBag->resolveValue($name)] = $parameterBag->resolveValue($target); }
}
$container->setAliases($aliases);
$parameterBag->resolve(); protected function processValue($value, $isRoot = false)
{
if (is_string($value)) {
return $this->bag->resolveValue($value);
}
if ($value instanceof Definition) {
$value->setClass($this->bag->resolveValue($value->getClass()));
$value->setFile($this->bag->resolveValue($value->getFile()));
$value->setProperties($this->bag->resolveValue($value->getProperties()));
$value->setMethodCalls($this->bag->resolveValue($value->getMethodCalls()));
}
return parent::processValue($value, $isRoot);
} }
} }

View File

@ -316,7 +316,7 @@ class ProjectServiceContainer extends Container
return $this->services['lazy_context'] = new \LazyContext(new RewindableGenerator(function() { return $this->services['lazy_context'] = new \LazyContext(new RewindableGenerator(function() {
yield 0 => 'foo'; yield 0 => 'foo';
yield 1 => ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->get('foo.baz')) && false ?: '_'}; yield 1 => ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->get('foo.baz')) && false ?: '_'};
yield 2 => array('bar' => 'foo is '.'bar'.'', 'foobar' => 'bar'); yield 2 => array('bar' => 'foo is bar', 'foobar' => 'bar');
yield 3 => true; yield 3 => true;
yield 4 => $this; yield 4 => $this;
})); }));