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>
*/
class AnalyzeServiceReferencesPass implements RepeatablePassInterface
class AnalyzeServiceReferencesPass extends AbstractRecursivePass implements RepeatablePassInterface
{
private $graph;
private $container;
private $currentId;
private $currentDefinition;
private $repeatedPass;
private $onlyConstructorArguments;
private $lazy;
/**
* @param bool $onlyConstructorArguments Sets this Service Reference pass to ignore method calls
@ -60,68 +59,60 @@ class AnalyzeServiceReferencesPass implements RepeatablePassInterface
$this->container = $container;
$this->graph = $container->getCompiler()->getServiceReferenceGraph();
$this->graph->clear();
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()));
}
}
}
$this->lazy = false;
foreach ($container->getAliases() as $id => $alias) {
$this->graph->connect($id, $alias, (string) $alias, $this->getDefinition((string) $alias), null);
}
parent::process($container);
}
/**
* 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)
protected function processValue($value, $isRoot = false)
{
foreach ($arguments as $argument) {
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);
$lazy = $this->lazy;
$this->graph->connect(
$this->currentId,
$this->currentDefinition,
$this->getDefinitionId((string) $argument),
$targetDefinition,
$argument,
$lazy || ($targetDefinition && $targetDefinition->isLazy())
);
} elseif ($argument instanceof Definition) {
$this->processArguments($argument->getArguments());
$this->processArguments($argument->getMethodCalls());
$this->processArguments($argument->getProperties());
if ($value instanceof ArgumentInterface) {
$this->lazy = true;
parent::processValue($value);
$this->lazy = $lazy;
if (is_array($argument->getFactory())) {
$this->processArguments($argument->getFactory());
}
}
return $value;
}
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>
*/
class AutowirePass implements CompilerPassInterface
class AutowirePass extends AbstractRecursivePass implements CompilerPassInterface
{
/**
* @var ContainerBuilder
*/
private $container;
private $reflectionClasses = array();
private $definedTypes = array();
private $types;
@ -42,17 +41,11 @@ class AutowirePass implements CompilerPassInterface
spl_autoload_register($throwingAutoloader);
try {
$this->container = $container;
foreach ($container->getDefinitions() as $id => $definition) {
if ($autowiredMethods = $definition->getAutowiredMethods()) {
$this->completeDefinition($id, $definition, $autowiredMethods);
}
}
parent::process($container);
} finally {
spl_autoload_unregister($throwingAutoloader);
// Free memory and remove circular reference to container
$this->container = null;
$this->reflectionClasses = array();
$this->definedTypes = array();
$this->types = null;
@ -85,18 +78,16 @@ class AutowirePass implements CompilerPassInterface
}
/**
* Wires the given definition.
*
* @param string $id
* @param Definition $definition
* @param string[] $autowiredMethods
*
* @throws RuntimeException
* {@inheritdoc}
*/
private function completeDefinition($id, Definition $definition, array $autowiredMethods)
protected function processValue($value, $isRoot = false)
{
if (!$reflectionClass = $this->getReflectionClass($id, $definition)) {
return;
if (!$value instanceof Definition || !$autowiredMethods = $value->getAutowiredMethods()) {
return parent::processValue($value, $isRoot);
}
if (!$reflectionClass = $this->getReflectionClass($isRoot ? $this->currentId : null, $value)) {
return parent::processValue($value, $isRoot);
}
if ($this->container->isTrackingResources()) {
@ -104,27 +95,28 @@ class AutowirePass implements CompilerPassInterface
}
$methodsCalled = array();
foreach ($definition->getMethodCalls() as $methodCall) {
$methodsCalled[$methodCall[0]] = true;
foreach ($value->getMethodCalls() as $methodCall) {
$methodsCalled[strtolower($methodCall[0])] = true;
}
foreach ($this->getMethodsToAutowire($id, $reflectionClass, $autowiredMethods) as $reflectionMethod) {
if (!isset($methodsCalled[$reflectionMethod->name])) {
$this->autowireMethod($id, $definition, $reflectionMethod);
foreach ($this->getMethodsToAutowire($reflectionClass, $autowiredMethods) as $reflectionMethod) {
if (!isset($methodsCalled[strtolower($reflectionMethod->name)])) {
$this->autowireMethod($value, $reflectionMethod);
}
}
return parent::processValue($value, $isRoot);
}
/**
* Gets the list of methods to autowire.
*
* @param string $id
* @param \ReflectionClass $reflectionClass
* @param string[] $configuredAutowiredMethods
*
* @return \ReflectionMethod[]
*/
private function getMethodsToAutowire($id, \ReflectionClass $reflectionClass, array $configuredAutowiredMethods)
private function getMethodsToAutowire(\ReflectionClass $reflectionClass, array $configuredAutowiredMethods)
{
$found = array();
$regexList = array();
@ -157,20 +149,19 @@ class AutowirePass implements CompilerPassInterface
if ($notFound = array_diff($configuredAutowiredMethods, $found)) {
$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.
*
* @param string $id
* @param Definition $definition
* @param \ReflectionMethod $reflectionMethod
*
* @throws RuntimeException
*/
private function autowireMethod($id, Definition $definition, \ReflectionMethod $reflectionMethod)
private function autowireMethod(Definition $definition, \ReflectionMethod $reflectionMethod)
{
if ($isConstructor = $reflectionMethod->isConstructor()) {
$arguments = $definition->getArguments();
@ -189,7 +180,7 @@ class AutowirePass implements CompilerPassInterface
// no default value? Then fail
if (!$parameter->isOptional()) {
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;
@ -210,7 +201,7 @@ class AutowirePass implements CompilerPassInterface
$addMethodCall = true;
} else {
try {
$value = $this->createAutowiredDefinition($typeHint, $id);
$value = $this->createAutowiredDefinition($typeHint);
$addMethodCall = true;
} catch (RuntimeException $e) {
if ($parameter->allowsNull()) {
@ -338,38 +329,40 @@ class AutowirePass implements CompilerPassInterface
* Registers a definition for the type if possible or throws an exception.
*
* @param \ReflectionClass $typeHint
* @param string $id
*
* @return Reference A reference to the registered definition
*
* @throws RuntimeException
*/
private function createAutowiredDefinition(\ReflectionClass $typeHint, $id)
private function createAutowiredDefinition(\ReflectionClass $typeHint)
{
if (isset($this->ambiguousServiceTypes[$typeHint->name])) {
$classOrInterface = $typeHint->isInterface() ? 'interface' : 'class';
$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()) {
$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->setPublic(false);
$argumentDefinition->setAutowired(true);
$this->populateAvailableType($argumentId, $argumentDefinition);
try {
$this->completeDefinition($argumentId, $argumentDefinition, array('__construct'));
$this->processValue($argumentDefinition, true);
$this->currentId = $currentId;
} catch (RuntimeException $e) {
$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);
}
@ -379,14 +372,14 @@ class AutowirePass implements CompilerPassInterface
/**
* Retrieves the reflection class associated with the given service.
*
* @param string $id
* @param Definition $definition
* @param string|null $id
* @param Definition $definition
*
* @return \ReflectionClass|false
*/
private function getReflectionClass($id, Definition $definition)
{
if (isset($this->reflectionClasses[$id])) {
if (null !== $id && isset($this->reflectionClasses[$id])) {
return $this->reflectionClasses[$id];
}
@ -403,7 +396,11 @@ class AutowirePass implements CompilerPassInterface
$reflector = false;
}
return $this->reflectionClasses[$id] = $reflector;
if (null !== $id) {
$this->reflectionClasses[$id] = $reflector;
}
return $reflector;
}
private function addServiceToAmbiguousType($id, $type)

View File

@ -11,56 +11,27 @@
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\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Checks that all references are pointing to a valid service.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class CheckExceptionOnInvalidReferenceBehaviorPass implements CompilerPassInterface
class CheckExceptionOnInvalidReferenceBehaviorPass extends AbstractRecursivePass implements CompilerPassInterface
{
private $container;
private $sourceId;
public function process(ContainerBuilder $container)
protected function processValue($value, $isRoot = false)
{
$this->container = $container;
if ($value instanceof Reference && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()) {
$destId = (string) $value;
foreach ($container->getDefinitions() as $id => $definition) {
$this->sourceId = $id;
$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);
}
if (!$this->container->has($destId)) {
throw new ServiceNotFoundException($destId, $this->currentId);
}
}
return parent::processValue($value, $isRoot);
}
}

View File

@ -11,10 +11,8 @@
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
/**
@ -25,75 +23,26 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException;
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class CheckReferenceValidityPass implements CompilerPassInterface
class CheckReferenceValidityPass extends AbstractRecursivePass implements CompilerPassInterface
{
private $container;
private $currentId;
/**
* Processes the ContainerBuilder to validate References.
*
* @param ContainerBuilder $container
*/
public function process(ContainerBuilder $container)
protected function processValue($value, $isRoot = false)
{
$this->container = $container;
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 ($isRoot && $value instanceof Definition && ($value->isSynthetic() || $value->isAbstract())) {
return $value;
}
}
if ($value instanceof Reference && $this->container->hasDefinition((string) $value)) {
$targetDefinition = $this->container->getDefinition((string) $value);
/**
* Validates an array of References.
*
* @param array $arguments An array of Reference objects
*
* @throws RuntimeException when there is a reference to an abstract definition.
*/
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
));
}
if ($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,
$value
));
}
}
}
/**
* 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);
return parent::processValue($value, $isRoot);
}
}

View File

@ -14,20 +14,15 @@ namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Inline service definitions where this is possible.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class InlineServiceDefinitionsPass implements RepeatablePassInterface
class InlineServiceDefinitionsPass extends AbstractRecursivePass implements RepeatablePassInterface
{
private $repeatedPass;
private $graph;
private $compiler;
private $formatter;
private $currentId;
/**
* {@inheritdoc}
@ -38,77 +33,38 @@ class InlineServiceDefinitionsPass implements RepeatablePassInterface
}
/**
* Processes the ContainerBuilder for inline service definitions.
*
* @param ContainerBuilder $container
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
protected function processValue($value, $isRoot = false)
{
$this->compiler = $container->getCompiler();
$this->formatter = $this->compiler->getLoggingFormatter();
$this->graph = $this->compiler->getServiceReferenceGraph();
if ($value instanceof ArgumentInterface) {
$this->processValue($value->getValues());
$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);
/**
* Processes inline arguments.
*
* @param ContainerBuilder $container The ContainerBuilder
* @param array $arguments An array of arguments
* @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;
if ($this->isInlineableDefinition($id, $definition, $compiler->getServiceReferenceGraph())) {
$compiler->addLogMessage($compiler->getLoggingFormatter()->formatInlineService($this, $id, $this->currentId));
if ($definition->isShared()) {
return $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]);
$value = clone $definition;
}
}
return $arguments;
return parent::processValue($value, $isRoot);
}
/**
* Checks if the definition is inlineable.
*
* @param string $id
* @param Definition $definition
*
* @return bool If the definition is inlineable
*/
private function isInlineableDefinition($id, Definition $definition)
private function isInlineableDefinition($id, Definition $definition, ServiceReferenceGraph $graph)
{
if (!$definition->isShared()) {
return true;
@ -118,7 +74,7 @@ class InlineServiceDefinitionsPass implements RepeatablePassInterface
return false;
}
if (!$this->graph->hasNode($id)) {
if (!$graph->hasNode($id)) {
return true;
}
@ -127,7 +83,7 @@ class InlineServiceDefinitionsPass implements RepeatablePassInterface
}
$ids = array();
foreach ($this->graph->getNode($id)->getInEdges() as $edge) {
foreach ($graph->getNode($id)->getInEdges() as $edge) {
$ids[] = $edge->getSourceNode()->getId();
}

View File

@ -11,7 +11,6 @@
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
@ -22,10 +21,9 @@ use Symfony\Component\DependencyInjection\Reference;
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ReplaceAliasByActualDefinitionPass implements CompilerPassInterface
class ReplaceAliasByActualDefinitionPass extends AbstractRecursivePass implements CompilerPassInterface
{
private $compiler;
private $formatter;
private $replacements;
/**
* Process the Container to replace aliases with service definitions.
@ -36,9 +34,6 @@ class ReplaceAliasByActualDefinitionPass implements CompilerPassInterface
*/
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
$seenAliasTargets = array();
$replacements = array();
@ -72,60 +67,25 @@ class ReplaceAliasByActualDefinitionPass implements CompilerPassInterface
$container->removeDefinition($targetId);
$replacements[$targetId] = $definitionId;
}
$this->replacements = $replacements;
// Now replace target instances in all definitions
foreach ($container->getDefinitions() as $definitionId => $definition) {
$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()));
}
parent::process($container);
$this->replacements = array();
}
/**
* Recursively updates references in an array.
*
* @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
* {@inheritdoc}
*/
private function updateArgumentReferences(array $replacements, $definitionId, array $arguments)
protected function processValue($value, $isRoot = false)
{
foreach ($arguments as $k => $argument) {
// 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;
}
if ($value instanceof Reference && isset($this->replacements[$referenceId = (string) $value])) {
// Perform the replacement
$newId = $replacements[$referenceId];
$arguments[$k] = new Reference($newId, $argument->getInvalidBehavior());
$this->compiler->addLogMessage($this->formatter->formatUpdateReference($this, $definitionId, $referenceId, $newId));
$newId = $this->replacements[$referenceId];
$value = new Reference($newId, $value->getInvalidBehavior());
$compiler = $this->container->getCompiler();
$compiler->addLogMessage($compiler->getLoggingFormatter()->formatUpdateReference($this, $this->currentId, $referenceId, $newId));
}
return $arguments;
}
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;
return parent::processValue($value, $isRoot);
}
}

View File

@ -11,10 +11,8 @@
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\ExceptionInterface;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
@ -25,83 +23,39 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException;
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class ResolveDefinitionTemplatesPass implements CompilerPassInterface
class ResolveDefinitionTemplatesPass extends AbstractRecursivePass implements CompilerPassInterface
{
private $compiler;
private $formatter;
private $currentId;
/**
* Process the ContainerBuilder to replace ChildDefinition instances with their real Definition instances.
*
* @param ContainerBuilder $container
*/
public function process(ContainerBuilder $container)
protected function processValue($value, $isRoot = false)
{
$this->compiler = $container->getCompiler();
$this->formatter = $this->compiler->getLoggingFormatter();
$container->setDefinitions($this->resolveArguments($container, $container->getDefinitions(), true));
}
/**
* Resolves definition decorator arguments.
*
* @param ContainerBuilder $container The ContainerBuilder
* @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 (!$value instanceof Definition) {
return parent::processValue($value, $isRoot);
}
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);
}
if ($value instanceof ChildDefinition) {
$value = $this->resolveDefinition($value);
if ($isRoot) {
// yes, we are specifically fetching the definition from the
// 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]);
$this->container->setDefinition($this->currentId, $value);
}
}
return $arguments;
return parent::processValue($value, $isRoot);
}
/**
* Resolves the definition.
*
* @param ContainerBuilder $container The ContainerBuilder
* @param ChildDefinition $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 {
return $this->doResolveDefinition($container, $definition);
return $this->doResolveDefinition($definition);
} catch (ExceptionInterface $e) {
$r = new \ReflectionProperty($e, 'message');
$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));
}
$parentDef = $container->findDefinition($parent);
$parentDef = $this->container->findDefinition($parent);
if ($parentDef instanceof ChildDefinition) {
$id = $this->currentId;
$this->currentId = $parent;
$parentDef = $this->resolveDefinition($container, $parentDef);
$container->setDefinition($parent, $parentDef);
$parentDef = $this->resolveDefinition($parentDef);
$this->container->setDefinition($parent, $parentDef);
$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();
// 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\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
@ -26,6 +27,7 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException;
class ResolveInvalidReferencesPass implements CompilerPassInterface
{
private $container;
private $signalingException;
/**
* Process the ContainerBuilder to resolve invalid references.
@ -35,86 +37,76 @@ class ResolveInvalidReferencesPass implements CompilerPassInterface
public function process(ContainerBuilder $container)
{
$this->container = $container;
foreach ($container->getDefinitions() as $definition) {
if ($definition->isSynthetic() || $definition->isAbstract()) {
continue;
}
$this->signalingException = new RuntimeException('Invalid reference.');
$definition->setArguments(
$this->processArguments($definition->getArguments())
);
$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);
try {
$this->processValue($container->getDefinitions(), 1);
} finally {
$this->container = $this->signalingException = null;
}
}
/**
* Processes arguments to determine invalid references.
*
* @param array $arguments An array of Reference objects
* @param bool $inMethodCall
* @param bool $inCollection
*
* @return array
*
* @throws RuntimeException When the config is invalid
* @throws RuntimeException When an invalid reference is found
*/
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) {
if (is_array($argument)) {
$arguments[$k] = $this->processArguments($argument, $inMethodCall, true);
} elseif ($argument instanceof ArgumentInterface) {
$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;
foreach ($value as $k => $v) {
try {
if (false !== $i && $k !== $i++) {
$i = false;
}
if ($inMethodCall) {
throw new RuntimeException('Method shouldn\'t be called.');
if ($v !== $processedValue = $this->processValue($v, $rootLevel, 1 + $level)) {
$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.
if ($isNumeric) {
$arguments = array_values($arguments);
}
return $arguments;
return $value;
}
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
/**
@ -19,52 +20,50 @@ use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
*
* @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.
*
* @param ContainerBuilder $container
* {@inheritdoc}
*
* @throws ParameterNotFoundException
*/
public function process(ContainerBuilder $container)
{
$parameterBag = $container->getParameterBag();
$this->bag = $container->getParameterBag();
foreach ($container->getDefinitions() as $id => $definition) {
try {
$definition->setClass($parameterBag->resolveValue($definition->getClass()));
$definition->setFile($parameterBag->resolveValue($definition->getFile()));
$definition->setArguments($parameterBag->resolveValue($definition->getArguments()));
try {
parent::process($container);
$factory = $definition->getFactory();
if (is_array($factory) && isset($factory[0])) {
$factory[0] = $parameterBag->resolveValue($factory[0]);
$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;
$aliases = array();
foreach ($container->getAliases() as $name => $target) {
$this->currentId = $name;
$aliases[$this->bag->resolveValue($name)] = $this->bag->resolveValue($target);
}
$container->setAliases($aliases);
} catch (ParameterNotFoundException $e) {
$e->setSourceId($this->currentId);
throw $e;
}
$aliases = array();
foreach ($container->getAliases() as $name => $target) {
$aliases[$parameterBag->resolveValue($name)] = $parameterBag->resolveValue($target);
}
$container->setAliases($aliases);
$this->bag->resolve();
$this->bag = null;
}
$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() {
yield 0 => 'foo';
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 4 => $this;
}));