From 0db3358ddb27918d0faa84fdd453e74257e9132f Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 30 Aug 2017 09:27:55 +0200 Subject: [PATCH] [DI] Add ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE --- .../DependencyInjection/CHANGELOG.md | 1 + .../Compiler/AnalyzeServiceReferencesPass.php | 4 +- ...xceptionOnInvalidReferenceBehaviorPass.php | 17 ++- .../Compiler/InlineServiceDefinitionsPass.php | 3 + .../RegisterServiceSubscribersPass.php | 3 + .../Compiler/RemoveUnusedDefinitionsPass.php | 3 + .../Compiler/ServiceReferenceGraph.php | 21 +-- .../Compiler/ServiceReferenceGraphEdge.php | 15 +- .../DependencyInjection/Container.php | 23 +-- .../DependencyInjection/ContainerBuilder.php | 48 +++++- .../ContainerInterface.php | 1 + .../DependencyInjection/Dumper/PhpDumper.php | 22 +-- .../DependencyInjection/Dumper/XmlDumper.php | 2 + .../DependencyInjection/Dumper/YamlDumper.php | 8 +- .../Loader/XmlFileLoader.php | 2 + .../Loader/YamlFileLoader.php | 3 + .../schema/dic/services/services-1.0.xsd | 1 + ...tionOnInvalidReferenceBehaviorPassTest.php | 21 +++ .../Tests/ContainerBuilderTest.php | 32 ++++ .../Tests/Dumper/PhpDumperTest.php | 38 +++++ .../Tests/Dumper/XmlDumperTest.php | 16 ++ .../Tests/Dumper/YamlDumperTest.php | 4 + .../container_uninitialized_ref.php | 46 ++++++ .../php/services_uninitialized_ref.php | 138 ++++++++++++++++++ .../Tests/Fixtures/xml/services_dump_load.xml | 11 ++ .../Fixtures/yaml/services_dump_load.yml | 1 + 26 files changed, 431 insertions(+), 53 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_uninitialized_ref.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_uninitialized_ref.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_dump_load.xml diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 49e140de11796..4232369e2e007 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 3.4.0 ----- + * added support for ignore-on-uninitialized references * deprecated service auto-registration while autowiring * deprecated the ability to check for the initialization of a private service with the `Container::initialized()` method * deprecated support for top-level anonymous services in XML diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php index 3a375f351652d..99eed81fcc4dd 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php @@ -12,6 +12,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\ExpressionLanguage; use Symfony\Component\DependencyInjection\Reference; @@ -96,7 +97,8 @@ protected function processValue($value, $isRoot = false) $this->getDefinitionId((string) $value), $targetDefinition, $value, - $this->lazy || ($targetDefinition && $targetDefinition->isLazy()) + $this->lazy || ($targetDefinition && $targetDefinition->isLazy()), + ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $value->getInvalidBehavior() ); return $value; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php index 35fb325e74964..7ffedd3dc0523 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DependencyInjection\Compiler; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; @@ -24,14 +25,16 @@ class CheckExceptionOnInvalidReferenceBehaviorPass extends AbstractRecursivePass { protected function processValue($value, $isRoot = false) { - if ($value instanceof Reference && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()) { - $destId = (string) $value; - - if (!$this->container->has($destId)) { - throw new ServiceNotFoundException($destId, $this->currentId); - } + if (!$value instanceof Reference) { + return parent::processValue($value, $isRoot); + } + if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior() && !$this->container->has($id = (string) $value)) { + throw new ServiceNotFoundException($id, $this->currentId); + } + if (ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $value->getInvalidBehavior() && $this->container->has($id = (string) $value) && !$this->container->findDefinition($id)->isShared()) { + throw new InvalidArgumentException(sprintf('Invalid ignore-on-uninitialized reference found in service "%s": target service "%s" is not shared.', $this->currentId, $id)); } - return parent::processValue($value, $isRoot); + return $value; } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php index f2ef363c837cc..240e1ab65527f 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php @@ -96,6 +96,9 @@ private function isInlineableDefinition($id, Definition $definition, ServiceRefe $ids = array(); foreach ($graph->getNode($id)->getInEdges() as $edge) { + if ($edge->isWeak()) { + return false; + } $ids[] = $edge->getSourceNode()->getId(); } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php b/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php index f8dba86a0b547..8c81452b315a6 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php @@ -74,6 +74,9 @@ protected function processValue($value, $isRoot = false) if ($optionalBehavior = '?' === $type[0]) { $type = substr($type, 1); $optionalBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; + } elseif ($optionalBehavior = '!' === $type[0]) { + $type = substr($type, 1); + $optionalBehavior = ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE; } if (is_int($key)) { $key = $type; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RemoveUnusedDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/RemoveUnusedDefinitionsPass.php index 79a2600d8f785..a8a01be6d5291 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/RemoveUnusedDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/RemoveUnusedDefinitionsPass.php @@ -50,6 +50,9 @@ public function process(ContainerBuilder $container) $referencingAliases = array(); $sourceIds = array(); foreach ($edges as $edge) { + if ($edge->isWeak()) { + continue; + } $node = $edge->getSourceNode(); $sourceIds[] = $node->getId(); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php index 193a37e20b0f6..3a6f03ee6d862 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php @@ -20,6 +20,8 @@ * it themselves which improves performance quite a lot. * * @author Johannes M. Schmitt + * + * @final since version 3.4 */ class ServiceReferenceGraph { @@ -85,23 +87,16 @@ public function clear() * @param string $destValue * @param string $reference * @param bool $lazy + * @param bool $weak */ - public function connect($sourceId, $sourceValue, $destId, $destValue = null, $reference = null/*, bool $lazy = false*/) + public function connect($sourceId, $sourceValue, $destId, $destValue = null, $reference = null/*, bool $lazy = false, bool $weak = false*/) { - if (func_num_args() >= 6) { - $lazy = func_get_arg(5); - } else { - if (__CLASS__ !== get_class($this)) { - $r = new \ReflectionMethod($this, __FUNCTION__); - if (__CLASS__ !== $r->getDeclaringClass()->getName()) { - @trigger_error(sprintf('Method %s() will have a 6th `bool $lazy = false` argument in version 4.0. Not defining it is deprecated since 3.3.', __METHOD__), E_USER_DEPRECATED); - } - } - $lazy = false; - } + $lazy = func_num_args() >= 6 ? func_get_arg(5) : false; + $weak = func_num_args() >= 7 ? func_get_arg(6) : false; + $sourceNode = $this->createNode($sourceId, $sourceValue); $destNode = $this->createNode($destId, $destValue); - $edge = new ServiceReferenceGraphEdge($sourceNode, $destNode, $reference, $lazy); + $edge = new ServiceReferenceGraphEdge($sourceNode, $destNode, $reference, $lazy, $weak); $sourceNode->addOutEdge($edge); $destNode->addInEdge($edge); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphEdge.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphEdge.php index 17dd5d9559f9d..5b8256977f8e4 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphEdge.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphEdge.php @@ -24,19 +24,22 @@ class ServiceReferenceGraphEdge private $destNode; private $value; private $lazy; + private $weak; /** * @param ServiceReferenceGraphNode $sourceNode * @param ServiceReferenceGraphNode $destNode * @param string $value * @param bool $lazy + * @param bool $weak */ - public function __construct(ServiceReferenceGraphNode $sourceNode, ServiceReferenceGraphNode $destNode, $value = null, $lazy = false) + public function __construct(ServiceReferenceGraphNode $sourceNode, ServiceReferenceGraphNode $destNode, $value = null, $lazy = false, $weak = false) { $this->sourceNode = $sourceNode; $this->destNode = $destNode; $this->value = $value; $this->lazy = $lazy; + $this->weak = $weak; } /** @@ -78,4 +81,14 @@ public function isLazy() { return $this->lazy; } + + /** + * Returns true if the edge is weak, meaning it shouldn't prevent removing the target service. + * + * @return bool + */ + public function isWeak() + { + return $this->weak; + } } diff --git a/src/Symfony/Component/DependencyInjection/Container.php b/src/Symfony/Component/DependencyInjection/Container.php index 30bf1a0a6ccea..ac423bd4a4739 100644 --- a/src/Symfony/Component/DependencyInjection/Container.php +++ b/src/Symfony/Component/DependencyInjection/Container.php @@ -23,26 +23,15 @@ * Container is a dependency injection container. * * It gives access to object instances (services). - * * Services and parameters are simple key/pair stores. - * - * Parameter and service keys are case insensitive. - * - * A service can also be defined by creating a method named - * getXXXService(), where XXX is the camelized version of the id: - * - * - * - * The container can have three possible behaviors when a service does not exist: + * The container can have four possible behaviors when a service + * does not exist (or is not initialized for the last case): * * * EXCEPTION_ON_INVALID_REFERENCE: Throws an exception (the default) * * NULL_ON_INVALID_REFERENCE: Returns null * * IGNORE_ON_INVALID_REFERENCE: Ignores the wrapping command asking for the reference * (for instance, ignore a setter if the service does not exist) + * * IGNORE_ON_UNINITIALIZED_REFERENCE: Ignores/returns null for uninitialized services or invalid references * * @author Fabien Potencier * @author Johannes M. Schmitt @@ -304,9 +293,9 @@ public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE try { if (isset($this->fileMap[$id])) { - return $this->load($this->fileMap[$id]); + return self::IGNORE_ON_UNINITIALIZED_REFERENCE === $invalidBehavior ? null : $this->load($this->fileMap[$id]); } elseif (isset($this->methodMap[$id])) { - return $this->{$this->methodMap[$id]}(); + return self::IGNORE_ON_UNINITIALIZED_REFERENCE === $invalidBehavior ? null : $this->{$this->methodMap[$id]}(); } elseif (--$i && $id !== $normalizedId = $this->normalizeId($id)) { $id = $normalizedId; continue; @@ -315,7 +304,7 @@ public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE // and only when the dumper has not generated the method map (otherwise the method map is considered to be fully populated by the dumper) @trigger_error('Generating a dumped container without populating the method map is deprecated since 3.2 and will be unsupported in 4.0. Update your dumper to generate the method map.', E_USER_DEPRECATED); - return $this->{$method}(); + return self::IGNORE_ON_UNINITIALIZED_REFERENCE === $invalidBehavior ? null : $this->{$method}(); } break; diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 683f8777e5c7e..2b8dfd6a3e520 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -565,6 +565,9 @@ public function get($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INV { $id = $this->normalizeId($id); + if (ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $invalidBehavior) { + return parent::get($id, $invalidBehavior); + } if ($service = parent::get($id, ContainerInterface::NULL_ON_INVALID_REFERENCE)) { return $service; } @@ -1160,6 +1163,11 @@ public function resolveServices($value) continue 2; } } + foreach (self::getInitializedConditionals($v) as $s) { + if (!$this->get($s, ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE)) { + continue 2; + } + } yield $k => $this->resolveServices($v); } @@ -1171,6 +1179,11 @@ public function resolveServices($value) continue 2; } } + foreach (self::getInitializedConditionals($v) as $s) { + if (!$this->get($s, ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE)) { + continue 2; + } + } ++$count; } @@ -1397,6 +1410,8 @@ public function log(CompilerPassInterface $pass, $message) * @param mixed $value An array of conditionals to return * * @return array An array of Service conditionals + * + * @internal since version 3.4 */ public static function getServiceConditionals($value) { @@ -1413,6 +1428,30 @@ public static function getServiceConditionals($value) return $services; } + /** + * Returns the initialized conditionals. + * + * @param mixed $value An array of conditionals to return + * + * @return array An array of uninitialized conditionals + * + * @internal + */ + public static function getInitializedConditionals($value) + { + $services = array(); + + if (is_array($value)) { + foreach ($value as $v) { + $services = array_unique(array_merge($services, self::getInitializedConditionals($v))); + } + } elseif ($value instanceof Reference && $value->getInvalidBehavior() === ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE) { + $services[] = (string) $value; + } + + return $services; + } + /** * Computes a reasonably unique hash of a value. * @@ -1465,13 +1504,16 @@ private function getProxyInstantiator() private function callMethod($service, $call) { - $services = self::getServiceConditionals($call[1]); - - foreach ($services as $s) { + foreach (self::getServiceConditionals($call[1]) as $s) { if (!$this->has($s)) { return; } } + foreach (self::getInitializedConditionals($call[1]) as $s) { + if (!$this->get($s, ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE)) { + return; + } + } call_user_func_array(array($service, $call[0]), $this->resolveServices($this->getParameterBag()->unescapeValue($this->getParameterBag()->resolveValue($call[1])))); } diff --git a/src/Symfony/Component/DependencyInjection/ContainerInterface.php b/src/Symfony/Component/DependencyInjection/ContainerInterface.php index cfbc828722d8a..2274ec7bb3266 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerInterface.php +++ b/src/Symfony/Component/DependencyInjection/ContainerInterface.php @@ -27,6 +27,7 @@ interface ContainerInterface extends PsrContainerInterface const EXCEPTION_ON_INVALID_REFERENCE = 1; const NULL_ON_INVALID_REFERENCE = 2; const IGNORE_ON_INVALID_REFERENCE = 3; + const IGNORE_ON_UNINITIALIZED_REFERENCE = 4; /** * Sets a service. diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 49cd09f02a0c3..49e90a4115cce 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -277,7 +277,7 @@ private function addServiceLocalTempVariables($cId, Definition $definition, arra if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE === $behavior[$id]) { $code .= sprintf($template, $name, $this->getServiceCall($id)); } else { - $code .= sprintf($template, $name, $this->getServiceCall($id, new Reference($id, ContainerInterface::NULL_ON_INVALID_REFERENCE))); + $code .= sprintf($template, $name, $this->getServiceCall($id, new Reference($id, $behavior[$id]))); } } } @@ -1295,12 +1295,14 @@ private function wrapServiceConditionals($value, $code) */ private function getServiceConditionals($value) { - if (!$services = ContainerBuilder::getServiceConditionals($value)) { - return null; - } - $conditions = array(); - foreach ($services as $service) { + foreach (ContainerBuilder::getInitializedConditionals($value) as $service) { + if (!$this->container->hasDefinition($service)) { + return 'false'; + } + $conditions[] = sprintf("isset(\$this->services['%s'])", $service); + } + foreach (ContainerBuilder::getServiceConditionals($value) as $service) { if ($this->container->hasDefinition($service) && !$this->container->getDefinition($service)->isPublic()) { continue; } @@ -1335,8 +1337,8 @@ private function getServiceCallsFromArguments(array $arguments, array &$calls, a } if (!isset($behavior[$id])) { $behavior[$id] = $argument->getInvalidBehavior(); - } elseif (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $behavior[$id]) { - $behavior[$id] = $argument->getInvalidBehavior(); + } else { + $behavior[$id] = min($behavior[$id], $argument->getInvalidBehavior()); } ++$calls[$id]; @@ -1665,7 +1667,9 @@ private function getServiceCall($id, Reference $reference = null) return '$this'; } - if ($this->asFiles && $this->container->hasDefinition($id)) { + if (null !== $reference && ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $reference->getInvalidBehavior()) { + $code = 'null'; + } elseif ($this->asFiles && $this->container->hasDefinition($id)) { if ($this->container->getDefinition($id)->isShared()) { $code = sprintf("\$this->load(__DIR__.'/%s.php')", $this->generateMethodName($id)); } else { diff --git a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php index c6b3be06a1402..2bfefeb2d1e23 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php @@ -309,6 +309,8 @@ private function convertParameters(array $parameters, $type, \DOMElement $parent $element->setAttribute('on-invalid', 'null'); } elseif ($behaviour == ContainerInterface::IGNORE_ON_INVALID_REFERENCE) { $element->setAttribute('on-invalid', 'ignore'); + } elseif ($behaviour == ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE) { + $element->setAttribute('on-invalid', 'ignore_uninitialized'); } } elseif ($value instanceof Definition) { $element->setAttribute('type', 'service'); diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php index 76f97315c793a..d8f07edc08e5c 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php @@ -304,8 +304,12 @@ private function dumpValue($value) */ private function getServiceCall($id, Reference $reference = null) { - if (null !== $reference && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $reference->getInvalidBehavior()) { - return sprintf('@?%s', $id); + if (null !== $reference) { + switch ($reference->getInvalidBehavior()) { + case ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE: break; + case ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE: return sprintf('@!%s', $id); + default: return sprintf('@?%s', $id); + } } return sprintf('@%s', $id); diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 0a5f9593d4ef0..a5ee960a70170 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -492,6 +492,8 @@ private function getArgumentsAsPhp(\DOMElement $node, $name, $file, $lowercase = $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; if ('ignore' == $onInvalid) { $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; + } elseif ('ignore_uninitialized' == $onInvalid) { + $invalidBehavior = ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE; } elseif ('null' == $onInvalid) { $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; } diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index 8a1c51f4e6fc1..b57ead2c89015 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -758,6 +758,9 @@ private function resolveServices($value, $file, $isParameter = false) if (0 === strpos($value, '@@')) { $value = substr($value, 1); $invalidBehavior = null; + } elseif (0 === strpos($value, '@!')) { + $value = substr($value, 2); + $invalidBehavior = ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE; } elseif (0 === strpos($value, '@?')) { $value = substr($value, 2); $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; diff --git a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd index aad40ac63138d..0be8658e3d547 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd +++ b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd @@ -266,6 +266,7 @@ + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php index 65a782a4a83fc..dc20b39bc4dca 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php @@ -67,6 +67,27 @@ public function testProcessThrowsExceptionOnInvalidReferenceFromInlinedDefinitio $this->process($container); } + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid ignore-on-uninitialized reference found in service + */ + public function testProcessThrowsExceptionOnNonSharedUninitializedReference() + { + $container = new ContainerBuilder(); + + $container + ->register('a', 'stdClass') + ->addArgument(new Reference('b', $container::IGNORE_ON_UNINITIALIZED_REFERENCE)) + ; + + $container + ->register('b', 'stdClass') + ->setShared(false) + ; + + $this->process($container); + } + private function process(ContainerBuilder $container) { $pass = new CheckExceptionOnInvalidReferenceBehaviorPass(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index 895e035185f06..54e3227d23f18 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -1098,6 +1098,38 @@ public function testServiceLocator() $this->assertSame($container->get('bar_service'), $foo->get('bar')); } + public function testUninitializedReference() + { + $container = include __DIR__.'/Fixtures/containers/container_uninitialized_ref.php'; + $container->compile(); + + $bar = $container->get('bar'); + + $this->assertNull($bar->foo1); + $this->assertNull($bar->foo2); + $this->assertNull($bar->foo3); + $this->assertNull($bar->closures[0]()); + $this->assertNull($bar->closures[1]()); + $this->assertNull($bar->closures[2]()); + $this->assertSame(array(), iterator_to_array($bar->iter)); + + $container = include __DIR__.'/Fixtures/containers/container_uninitialized_ref.php'; + $container->compile(); + + $container->get('foo1'); + $container->get('baz'); + + $bar = $container->get('bar'); + + $this->assertEquals(new \stdClass(), $bar->foo1); + $this->assertNull($bar->foo2); + $this->assertEquals(new \stdClass(), $bar->foo3); + $this->assertEquals(new \stdClass(), $bar->closures[0]()); + $this->assertNull($bar->closures[1]()); + $this->assertEquals(new \stdClass(), $bar->closures[2]()); + $this->assertEquals(array('foo1' => new \stdClass(), 'foo3' => new \stdClass()), iterator_to_array($bar->iter)); + } + public function testRegisterForAutoconfiguration() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 338d7b5bb383b..62213ee2804bc 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -631,6 +631,44 @@ public function testExpressionReferencingPrivateService() $this->assertStringEqualsFile(self::$fixturesPath.'/php/services_private_in_expression.php', $dumper->dump()); } + public function testUninitializedReference() + { + $container = include self::$fixturesPath.'/containers/container_uninitialized_ref.php'; + $container->compile(); + $dumper = new PhpDumper($container); + + $this->assertStringEqualsFile(self::$fixturesPath.'/php/services_uninitialized_ref.php', $dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_Uninitialized_Reference'))); + + require self::$fixturesPath.'/php/services_uninitialized_ref.php'; + + $container = new \Symfony_DI_PhpDumper_Test_Uninitialized_Reference(); + + $bar = $container->get('bar'); + + $this->assertNull($bar->foo1); + $this->assertNull($bar->foo2); + $this->assertNull($bar->foo3); + $this->assertNull($bar->closures[0]()); + $this->assertNull($bar->closures[1]()); + $this->assertNull($bar->closures[2]()); + $this->assertSame(array(), iterator_to_array($bar->iter)); + + $container = new \Symfony_DI_PhpDumper_Test_Uninitialized_Reference(); + + $container->get('foo1'); + $container->get('baz'); + + $bar = $container->get('bar'); + + $this->assertEquals(new \stdClass(), $bar->foo1); + $this->assertNull($bar->foo2); + $this->assertEquals(new \stdClass(), $bar->foo3); + $this->assertEquals(new \stdClass(), $bar->closures[0]()); + $this->assertNull($bar->closures[1]()); + $this->assertEquals(new \stdClass(), $bar->closures[2]()); + $this->assertEquals(array('foo1' => new \stdClass(), 'foo3' => new \stdClass()), iterator_to_array($bar->iter)); + } + public function testDumpHandlesLiteralClassWithRootNamespace() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php index 3ac6628b737e6..8a34a2b19a297 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php @@ -12,8 +12,12 @@ namespace Symfony\Component\DependencyInjection\Tests\Dumper; use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Dumper\XmlDumper; +use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Reference; class XmlDumperTest extends TestCase { @@ -184,6 +188,18 @@ public function testDumpAutowireData() $this->assertEquals(file_get_contents(self::$fixturesPath.'/xml/services24.xml'), $dumper->dump()); } + public function testDumpLoad() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services_dump_load.xml'); + + $this->assertEquals(array(new Reference('bar', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE)), $container->getDefinition('foo')->getArguments()); + + $dumper = new XmlDumper($container); + $this->assertStringEqualsFile(self::$fixturesPath.'/xml/services_dump_load.xml', $dumper->dump()); + } + public function testDumpAbstractServices() { $container = include self::$fixturesPath.'/containers/container_abstract.php'; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php index 968385633b549..85ce181461419 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php @@ -14,9 +14,11 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Dumper\YamlDumper; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; +use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Parser; @@ -73,6 +75,8 @@ public function testDumpLoad() $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); $loader->load('services_dump_load.yml'); + $this->assertEquals(array(new Reference('bar', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE)), $container->getDefinition('foo')->getArguments()); + $dumper = new YamlDumper($container); $this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services_dump_load.yml', $dumper->dump()); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_uninitialized_ref.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_uninitialized_ref.php new file mode 100644 index 0000000000000..9ecf7c909f04a --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_uninitialized_ref.php @@ -0,0 +1,46 @@ +register('foo1', 'stdClass') +; + +$container + ->register('foo2', 'stdClass') + ->setPublic(false) +; + +$container + ->register('foo3', 'stdClass') + ->setPublic(false) +; + +$container + ->register('baz', 'stdClass') + ->setProperty('foo3', new Reference('foo3')) +; + +$container + ->register('bar', 'stdClass') + ->setProperty('foo1', new Reference('foo1', $container::IGNORE_ON_UNINITIALIZED_REFERENCE)) + ->setProperty('foo2', new Reference('foo2', $container::IGNORE_ON_UNINITIALIZED_REFERENCE)) + ->setProperty('foo3', new Reference('foo3', $container::IGNORE_ON_UNINITIALIZED_REFERENCE)) + ->setProperty('closures', array( + new ServiceClosureArgument(new Reference('foo1', $container::IGNORE_ON_UNINITIALIZED_REFERENCE)), + new ServiceClosureArgument(new Reference('foo2', $container::IGNORE_ON_UNINITIALIZED_REFERENCE)), + new ServiceClosureArgument(new Reference('foo3', $container::IGNORE_ON_UNINITIALIZED_REFERENCE)), + )) + ->setProperty('iter', new IteratorArgument(array( + 'foo1' => new Reference('foo1', $container::IGNORE_ON_UNINITIALIZED_REFERENCE), + 'foo2' => new Reference('foo2', $container::IGNORE_ON_UNINITIALIZED_REFERENCE), + 'foo3' => new Reference('foo3', $container::IGNORE_ON_UNINITIALIZED_REFERENCE), + ))) +; + +return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_uninitialized_ref.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_uninitialized_ref.php new file mode 100644 index 0000000000000..df0d74c07cbeb --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_uninitialized_ref.php @@ -0,0 +1,138 @@ +services = array(); + $this->methodMap = array( + 'bar' => 'getBarService', + 'baz' => 'getBazService', + 'foo1' => 'getFoo1Service', + 'foo3' => 'getFoo3Service', + ); + $this->privates = array( + 'foo3' => true, + ); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + /** + * {@inheritdoc} + */ + public function isCompiled() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function isFrozen() + { + @trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0. Use the isCompiled() method instead.', __METHOD__), E_USER_DEPRECATED); + + return true; + } + + /** + * Gets the public 'bar' shared service. + * + * @return \stdClass + */ + protected function getBarService() + { + $this->services['bar'] = $instance = new \stdClass(); + + $instance->foo1 = ${($_ = isset($this->services['foo1']) ? $this->services['foo1'] : null) && false ?: '_'}; + $instance->foo2 = null; + $instance->foo3 = ${($_ = isset($this->services['foo3']) ? $this->services['foo3'] : null) && false ?: '_'}; + $instance->closures = array(0 => function () { + return ${($_ = isset($this->services['foo1']) ? $this->services['foo1'] : null) && false ?: '_'}; + }, 1 => function () { + return null; + }, 2 => function () { + return ${($_ = isset($this->services['foo3']) ? $this->services['foo3'] : null) && false ?: '_'}; + }); + $instance->iter = new RewindableGenerator(function () { + if (isset($this->services['foo1'])) { + yield 'foo1' => ${($_ = isset($this->services['foo1']) ? $this->services['foo1'] : null) && false ?: '_'}; + } + if (false) { + yield 'foo2' => null; + } + if (isset($this->services['foo3'])) { + yield 'foo3' => ${($_ = isset($this->services['foo3']) ? $this->services['foo3'] : null) && false ?: '_'}; + } + }, function () { + return 0 + (int) (isset($this->services['foo1'])) + (int) (false) + (int) (isset($this->services['foo3'])); + }); + + return $instance; + } + + /** + * Gets the public 'baz' shared service. + * + * @return \stdClass + */ + protected function getBazService() + { + $this->services['baz'] = $instance = new \stdClass(); + + $instance->foo3 = ${($_ = isset($this->services['foo3']) ? $this->services['foo3'] : $this->getFoo3Service()) && false ?: '_'}; + + return $instance; + } + + /** + * Gets the public 'foo1' shared service. + * + * @return \stdClass + */ + protected function getFoo1Service() + { + return $this->services['foo1'] = new \stdClass(); + } + + /** + * Gets the private 'foo3' shared service. + * + * @return \stdClass + */ + protected function getFoo3Service() + { + return $this->services['foo3'] = new \stdClass(); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_dump_load.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_dump_load.xml new file mode 100644 index 0000000000000..e763aa870a028 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_dump_load.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_dump_load.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_dump_load.yml index 43b0c7d58a00f..8f3e153afcea4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_dump_load.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_dump_load.yml @@ -6,6 +6,7 @@ services: foo: autoconfigure: true abstract: true + arguments: ['@!bar'] Psr\Container\ContainerInterface: alias: service_container public: false