From cb498580d107051a5b7ab1ee7c8c88206165879f Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sat, 17 Dec 2016 16:24:27 +0100 Subject: [PATCH] [DI] Add getter injection --- .../DependencyInjection/CHANGELOG.md | 1 + .../Compiler/AbstractRecursivePass.php | 1 + .../ResolveDefinitionTemplatesPass.php | 6 + .../Compiler/ResolveInvalidReferencesPass.php | 1 + .../ResolveReferencesToAliasesPass.php | 1 + .../DependencyInjection/ContainerBuilder.php | 124 +++++++++++- .../DependencyInjection/Definition.php | 32 +++ .../Dumper/GraphvizDumper.php | 7 + .../DependencyInjection/Dumper/PhpDumper.php | 183 ++++++++++++++---- .../DependencyInjection/Dumper/XmlDumper.php | 4 + .../DependencyInjection/Dumper/YamlDumper.php | 4 + .../LazyProxy/GetterProxyInterface.php | 23 +++ .../Loader/XmlFileLoader.php | 3 +- .../Loader/YamlFileLoader.php | 5 + .../schema/dic/services/services-1.0.xsd | 13 ++ .../ResolveInvalidReferencesPassTest.php | 13 ++ .../Tests/ContainerBuilderTest.php | 53 +++++ .../Tests/Dumper/PhpDumperTest.php | 61 ++++++ .../Tests/Fixtures/containers/container29.php | 57 ++++++ .../Tests/Fixtures/containers/container30.php | 42 ++++ .../Tests/Fixtures/php/services29.php | 152 +++++++++++++++ .../Tests/Fixtures/xml/services31.xml | 10 + .../Tests/Fixtures/yaml/services31.yml | 6 + .../Tests/Loader/XmlFileLoaderTest.php | 9 + .../Tests/Loader/YamlFileLoaderTest.php | 9 + .../Component/VarDumper/Caster/ClassStub.php | 14 ++ .../VarDumper/Caster/SymfonyCaster.php | 16 ++ .../VarDumper/Cloner/AbstractCloner.php | 1 + 28 files changed, 811 insertions(+), 40 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/LazyProxy/GetterProxyInterface.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container29.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container30.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services29.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services31.xml create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services31.yml diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index ad32230ff59fe..e41bccbd8548a 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 3.3.0 ----- + * [EXPERIMENTAL] added support for getter-injection * added support for omitting the factory class name in a service definition if the definition class is set * deprecated case insensitivity of service identifiers * added "iterator" argument type for lazy iteration over a set of values and services diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php index 11a2197d04b8e..73ed78bce698c 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php @@ -61,6 +61,7 @@ protected function processValue($value, $isRoot = false) } elseif ($value instanceof Definition) { $value->setArguments($this->processValue($value->getArguments())); $value->setProperties($this->processValue($value->getProperties())); + $value->setOverriddenGetters($this->processValue($value->getOverriddenGetters())); $value->setMethodCalls($this->processValue($value->getMethodCalls())); if ($v = $value->getFactory()) { diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php index a05acece53a9e..b4165bfb5fb90 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php @@ -89,6 +89,7 @@ private function doResolveDefinition(ChildDefinition $definition) $def->setClass($parentDef->getClass()); $def->setArguments($parentDef->getArguments()); $def->setMethodCalls($parentDef->getMethodCalls()); + $def->setOverriddenGetters($parentDef->getOverriddenGetters()); $def->setProperties($parentDef->getProperties()); $def->setAutowiringTypes($parentDef->getAutowiringTypes()); if ($parentDef->isDeprecated()) { @@ -161,6 +162,11 @@ private function doResolveDefinition(ChildDefinition $definition) $def->setMethodCalls(array_merge($def->getMethodCalls(), $calls)); } + // merge overridden getters + foreach ($definition->getOverriddenGetters() as $k => $v) { + $def->setOverriddenGetter($k, $v); + } + // merge autowiring types foreach ($definition->getAutowiringTypes() as $autowiringType) { $def->addAutowiringType($autowiringType); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php index 7e31cdaafe357..abab663a0a684 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php @@ -59,6 +59,7 @@ private function processValue($value, $rootLevel = 0, $level = 0) } $value->setArguments($this->processValue($value->getArguments(), 0)); $value->setProperties($this->processValue($value->getProperties(), 1)); + $value->setOverriddenGetters($this->processValue($value->getOverriddenGetters(), 1)); $value->setMethodCalls($this->processValue($value->getMethodCalls(), 2)); } elseif (is_array($value)) { $i = 0; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php index ad4c43c8565ac..9fc4b7a92d325 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php @@ -42,6 +42,7 @@ public function process(ContainerBuilder $container) $definition->setArguments($this->processArguments($definition->getArguments())); $definition->setMethodCalls($this->processArguments($definition->getMethodCalls())); + $definition->setOverriddenGetters($this->processArguments($definition->getOverriddenGetters())); $definition->setProperties($this->processArguments($definition->getProperties())); $definition->setFactory($this->processFactory($definition->getFactory())); } diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index ee342f13a6a7c..5bc89113ba62a 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -29,6 +29,7 @@ use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface; use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\RealServiceInstantiator; +use Symfony\Component\DependencyInjection\LazyProxy\GetterProxyInterface; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; @@ -890,6 +891,9 @@ private function createService(Definition $definition, $id, $tryProxy = true) $arguments = $this->resolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($definition->getArguments()))); if (null !== $factory = $definition->getFactory()) { + if ($definition->getOverriddenGetters()) { + throw new RuntimeException(sprintf('Cannot create service "%s": factories and overridden getters are incompatible with each other.', $id)); + } if (is_array($factory)) { $factory = array($this->resolveServices($parameterBag->resolveValue($factory[0])), $factory[1]); } elseif (!is_string($factory)) { @@ -908,11 +912,31 @@ private function createService(Definition $definition, $id, $tryProxy = true) } else { $r = new \ReflectionClass($parameterBag->resolveValue($definition->getClass())); - $service = null === $r->getConstructor() ? $r->newInstance() : $r->newInstanceArgs($arguments); - if (!$definition->isDeprecated() && 0 < strpos($r->getDocComment(), "\n * @deprecated ")) { @trigger_error(sprintf('The "%s" service relies on the deprecated "%s" class. It should either be deprecated or its implementation upgraded.', $id, $r->name), E_USER_DEPRECATED); } + if ($definition->getOverriddenGetters()) { + static $salt; + if (null === $salt) { + $salt = str_replace('.', '', uniqid('', true)); + } + $service = sprintf('%s implements \\%s { private $container%4$s; private $values%4$s; %s }', $r->name, GetterProxyInterface::class, $this->generateOverriddenGetters($id, $definition, $r, $salt), $salt); + if (!class_exists($proxyClass = 'SymfonyProxy_'.md5($service), false)) { + eval(sprintf('class %s extends %s', $proxyClass, $service)); + } + $r = new \ReflectionClass($proxyClass); + $constructor = $r->getConstructor(); + if ($constructor && !defined('HHVM_VERSION') && $constructor->getDeclaringClass()->isInternal()) { + $constructor = null; + } + $service = $constructor ? $r->newInstanceWithoutConstructor() : $r->newInstanceArgs($arguments); + call_user_func(\Closure::bind(function ($c, $v, $s) { $this->{'container'.$s} = $c; $this->{'values'.$s} = $v; }, $service, $service), $this, $definition->getOverriddenGetters(), $salt); + if ($constructor) { + $constructor->invokeArgs($service, $arguments); + } + } else { + $service = null === $r->getConstructor() ? $r->newInstance() : $r->newInstanceArgs($arguments); + } } if ($tryProxy || !$definition->isLazy()) { @@ -1155,6 +1179,102 @@ public function getEnvCounters() return $this->envCounters; } + private function generateOverriddenGetters($id, Definition $definition, \ReflectionClass $class, $salt) + { + if ($class->isFinal()) { + throw new RuntimeException(sprintf('Unable to configure getter injection for service "%s": class "%s" cannot be marked as final.', $id, $class->name)); + } + $getters = ''; + foreach ($definition->getOverriddenGetters() as $name => $returnValue) { + $r = self::getGetterReflector($class, $name, $id, $type); + $visibility = $r->isProtected() ? 'protected' : 'public'; + $name = var_export($name, true); + $getters .= <<name}(){$type} { + \$c = \$this->container{$salt}; + \$b = \$c->getParameterBag(); + \$v = \$this->values{$salt}[{$name}]; + + foreach (\$c->getServiceConditionals(\$v) as \$s) { + if (!\$c->has(\$s)) { + return parent::{$r->name}(); + } + } + + return \$c->resolveServices(\$b->unescapeValue(\$b->resolveValue(\$v))); +} +EOF; + } + + return $getters; + } + + /** + * @internal + */ + public static function getGetterReflector(\ReflectionClass $class, $name, $id, &$type) + { + if (!$class->hasMethod($name)) { + throw new RuntimeException(sprintf('Unable to configure getter injection for service "%s": method "%s::%s" does not exist.', $id, $class->name, $name)); + } + $r = $class->getMethod($name); + if ($r->isPrivate()) { + throw new RuntimeException(sprintf('Unable to configure getter injection for service "%s": method "%s::%s" must be public or protected.', $id, $class->name, $r->name)); + } + if ($r->isStatic()) { + throw new RuntimeException(sprintf('Unable to configure getter injection for service "%s": method "%s::%s" cannot be static.', $id, $class->name, $r->name)); + } + if ($r->isFinal()) { + throw new RuntimeException(sprintf('Unable to configure getter injection for service "%s": method "%s::%s" cannot be marked as final.', $id, $class->name, $r->name)); + } + if ($r->returnsReference()) { + throw new RuntimeException(sprintf('Unable to configure getter injection for service "%s": method "%s::%s" cannot return by reference.', $id, $class->name, $r->name)); + } + if (0 < $r->getNumberOfParameters()) { + throw new RuntimeException(sprintf('Unable to configure getter injection for service "%s": method "%s::%s" cannot have any arguments.', $id, $class->name, $r->name)); + } + if ($type = method_exists($r, 'getReturnType') ? $r->getReturnType() : null) { + $type = ': '.($type->allowsNull() ? '?' : '').self::generateTypeHint($type, $r); + } + + return $r; + } + + /** + * @internal + */ + public static function generateTypeHint($type, \ReflectionFunctionAbstract $r) + { + if (is_string($type)) { + $name = $type; + + if ('callable' === $name || 'array' === $name) { + return $name; + } + } else { + $name = $type instanceof \ReflectionNamedType ? $type->getName() : $type->__toString(); + + if ($type->isBuiltin()) { + return $name; + } + } + $lcName = strtolower($name); + + if ('self' !== $lcName && 'parent' !== $lcName) { + return '\\'.$name; + } + if (!$r instanceof \ReflectionMethod) { + return; + } + if ('self' === $lcName) { + return '\\'.$r->getDeclaringClass()->name; + } + if ($parent = $r->getDeclaringClass()->getParentClass()) { + return '\\'.$parent->name; + } + } + /** * @internal */ diff --git a/src/Symfony/Component/DependencyInjection/Definition.php b/src/Symfony/Component/DependencyInjection/Definition.php index 69fcd4bc59671..f8ec6e9a88be9 100644 --- a/src/Symfony/Component/DependencyInjection/Definition.php +++ b/src/Symfony/Component/DependencyInjection/Definition.php @@ -29,6 +29,7 @@ class Definition private $deprecationTemplate = 'The "%service_id%" service is deprecated. You should stop using it, as it will soon be removed.'; private $properties = array(); private $calls = array(); + private $getters = array(); private $configurator; private $tags = array(); private $public = true; @@ -319,6 +320,37 @@ public function getMethodCalls() return $this->calls; } + /** + * @experimental in version 3.3 + */ + public function setOverriddenGetter($name, $returnValue) + { + if (!$name) { + throw new InvalidArgumentException(sprintf('Getter name cannot be empty.')); + } + $this->getters[strtolower($name)] = $returnValue; + + return $this; + } + + /** + * @experimental in version 3.3 + */ + public function setOverriddenGetters(array $getters) + { + $this->getters = array_change_key_case($getters, CASE_LOWER); + + return $this; + } + + /** + * @experimental in version 3.3 + */ + public function getOverriddenGetters() + { + return $this->getters; + } + /** * Sets tags for this definition. * diff --git a/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php index 9a20525f626c0..2305387349fc6 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php @@ -80,6 +80,13 @@ public function dump(array $options = array()) $this->findEdges($id, $call[1], false, $call[0].'()') ); } + + foreach ($definition->getOverriddenGetters() as $name => $value) { + $this->edges[$id] = array_merge( + $this->edges[$id], + $this->findEdges($id, $value, false, $name.'()') + ); + } } return $this->container->resolveEnvPlaceholders($this->startDot().$this->addNodes().$this->addEdges().$this->endDot(), '__ENV_%s__'); diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 5f836b0544d66..0597bd8c426ad 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -24,6 +24,7 @@ use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; +use Symfony\Component\DependencyInjection\LazyProxy\GetterProxyInterface; use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface as ProxyDumper; use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\NullDumper; use Symfony\Component\DependencyInjection\ExpressionLanguage; @@ -65,6 +66,9 @@ class PhpDumper extends Dumper private $usedMethodNames; private $classResources = array(); private $baseClass; + private $getterProxies = array(); + private $useInstantiateProxy; + private $salt; /** * @var \Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface @@ -120,6 +124,9 @@ public function dump(array $options = array()) 'debug' => true, ), $options); + $this->salt = substr(strtr(base64_encode(md5($options['namespace'].'\\'.$options['class'].'+'.$options['base_class'], true)), '+/', '__'), 0, -2); + $this->getterProxies = array(); + $this->useInstantiateProxy = false; $this->classResources = array(); $this->initializeMethodNamesMap($options['base_class']); $this->baseClass = $options['base_class']; @@ -168,6 +175,12 @@ public function dump(array $options = array()) $this->addProxyClasses() ; $this->targetDirRegex = null; + $this->getterProxies = array(); + + foreach ($this->classResources as $r) { + $this->container->addClassResource($r); + } + $this->classResources = array(); foreach ($this->classResources as $r) { $this->container->addClassResource($r); @@ -276,6 +289,10 @@ private function addProxyClasses() $code .= $proxyCode; } + foreach ($this->getterProxies as $proxyClass => $proxyCode) { + $code .= sprintf("\nclass %s extends %s", $proxyClass, $proxyCode); + } + return $code; } @@ -489,6 +506,72 @@ private function addServiceMethodCalls($id, Definition $definition, $variableNam return $calls; } + private function addServiceOverriddenGetters($id, Definition $definition) + { + if (!isset($this->classResources[$class = $definition->getClass()])) { + $this->classResources[$class] = new \ReflectionClass($class); + } + $class = $this->classResources[$class]; + if ($class->isFinal()) { + throw new RuntimeException(sprintf('Unable to configure getter injection for service "%s": class "%s" cannot be marked as final.', $id, $class->name)); + } + + if ($r = $class->getConstructor()) { + if ($r->isAbstract()) { + throw new RuntimeException(sprintf('Unable to configure service "%s": the constructor of the "%s" class cannot be abstract.', $id, $class->name)); + } + if (!$r->isPublic()) { + throw new RuntimeException(sprintf('Unable to configure service "%s": the constructor of the "%s" class must be public.', $id, $class->name)); + } + if (!$r->isFinal()) { + if (0 < $r->getNumberOfParameters()) { + $getters = implode('($container'.$this->salt.', ', explode('(', $this->generateSignature($r), 2)); + } else { + $getters = '($container'.$this->salt.')'; + } + $getters = sprintf("\n public function %s\n {\n \$this->container%3\$s = \$container%3\$s;\n parent::%s;\n }\n", $getters, $this->generateCall($r), $this->salt); + } else { + $getters = ''; + } + } else { + $getters = sprintf("\n public function __construct(\$container%1\$s)\n {\n \$this->container%1\$s = \$container%1\$s;\n }\n", $this->salt); + } + + foreach ($definition->getOverriddenGetters() as $name => $returnValue) { + $r = ContainerBuilder::getGetterReflector($class, $name, $id, $type); + $getter = array(); + $getter[] = sprintf('%s function %s()%s', $r->isProtected() ? 'protected' : 'public', $r->name, $type); + $getter[] = '{'; + + if (false === strpos($dumpedReturnValue = $this->dumpValue($returnValue), '$this')) { + $getter[] = sprintf(' return %s;', $dumpedReturnValue); + } else { + $getter[] = sprintf(' if (null === $g = &$this->getters%s[__FUNCTION__]) {', $this->salt); + $getter[] = sprintf(' $g = \Closure::bind(function () { return %s; }, %2$s, %2$s);', $dumpedReturnValue, '$this->container'.$this->salt); + $getter[] = ' }'; + $getter[] = ''; + foreach (explode("\n", $this->wrapServiceConditionals($returnValue, " return \$g();\n", $isUnconditional, '$this->container'.$this->salt)) as $code) { + if ($code) { + $getter[] = substr($code, 4); + } + } + if (!$isUnconditional) { + $getter[] = ''; + $getter[] = sprintf(' return parent::%s();', $r->name); + } + } + + $getter[] = '}'; + $getter[] = ''; + + foreach ($getter as $code) { + $getters .= $code ? "\n ".$code : "\n"; + } + } + + return $getters; + } + private function addServiceProperties($id, Definition $definition, $variableName = 'instance') { $code = ''; @@ -734,6 +817,9 @@ private function addNewInstance(Definition $definition, $return, $instantiation, } if (null !== $definition->getFactory()) { + if ($definition->getOverriddenGetters()) { + throw new RuntimeException(sprintf('Cannot dump definition for service "%s": factories and overridden getters are incompatible with each other.', $id)); + } $callable = $definition->getFactory(); if (is_array($callable)) { if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $callable[1])) { @@ -766,10 +852,30 @@ private function addNewInstance(Definition $definition, $return, $instantiation, } if (false !== strpos($class, '$')) { + if ($definition->getOverriddenGetters()) { + throw new RuntimeException(sprintf('Cannot dump definition for service "%s": dynamic class names and overridden getters are incompatible with each other.', $id)); + } + return sprintf(" \$class = %s;\n\n $return{$instantiation}new \$class(%s);\n", $class, implode(', ', $arguments)); } + $class = $this->dumpLiteralClass($class); + + if ($definition->getOverriddenGetters()) { + $getterProxy = sprintf("%s implements \\%s\n{\n private \$container%s;\n private \$getters%3\$s;\n%s}\n", $class, GetterProxyInterface::class, $this->salt, $this->addServiceOverriddenGetters($id, $definition)); + $class = 'SymfonyProxy_'.md5($getterProxy); + $this->getterProxies[$class] = $getterProxy; + $constructor = $this->classResources[$definition->getClass()]->getConstructor(); - return sprintf(" $return{$instantiation}new %s(%s);\n", $this->dumpLiteralClass($class), implode(', ', $arguments)); + if ($constructor && $constructor->isFinal()) { + $this->useInstantiateProxy = true; + $useConstructor = $constructor->getDeclaringClass()->isInternal() ? "defined('HHVM_VERSION')" : 'true'; + + return sprintf(" $return{$instantiation}\$this->instantiateProxy(%s::class, array(%s), %s);\n", $class, implode(', ', $arguments), $useConstructor); + } + array_unshift($arguments, '$this'); + } + + return sprintf(" $return{$instantiation}new %s(%s);\n", $class, implode(', ', $arguments)); } /** @@ -1210,6 +1316,34 @@ private function exportParameters(array $parameters, $path = '', $indent = 12) */ private function endClass() { + if ($this->useInstantiateProxy) { + return sprintf(<<<'EOF' + + private function instantiateProxy($class, $args, $useConstructor) + { + static $reflectionCache; + + if (null === $r = &$reflectionCache[$class]) { + $r[0] = new \ReflectionClass($class); + $r[1] = $r[0]->getProperty('container%s'); + $r[1]->setAccessible(true); + $r[2] = $r[0]->getConstructor(); + } + $service = $useConstructor ? $r[0]->newInstanceWithoutConstructor() : $r[0]->newInstanceArgs($args); + $r[1]->setValue($service, $this); + if ($r[2] && $useConstructor) { + $r[2]->invokeArgs($service, $args); + } + + return $service; + } +} + +EOF + , $this->salt + ); + } + return <<<'EOF' } @@ -1221,18 +1355,20 @@ private function endClass() * * @param string $value * @param string $code + * @param bool &$isUnconditional + * @param string $containerRef * * @return string */ - private function wrapServiceConditionals($value, $code) + private function wrapServiceConditionals($value, $code, &$isUnconditional = null, $containerRef = '$this') { - if (!$services = ContainerBuilder::getServiceConditionals($value)) { + if ($isUnconditional = !$services = ContainerBuilder::getServiceConditionals($value)) { return $code; } $conditions = array(); foreach ($services as $service) { - $conditions[] = sprintf("\$this->has('%s')", $service); + $conditions[] = sprintf("%s->has('%s')", $containerRef, $service); } // re-indent the wrapped code @@ -1283,6 +1419,7 @@ private function getInlinedDefinitions(Definition $definition) $definitions = array_merge( $this->getDefinitionsFromArguments($definition->getArguments()), $this->getDefinitionsFromArguments($definition->getMethodCalls()), + $this->getDefinitionsFromArguments($definition->getOverriddenGetters()), $this->getDefinitionsFromArguments($definition->getProperties()), $this->getDefinitionsFromArguments(array($definition->getConfigurator())), $this->getDefinitionsFromArguments(array($definition->getFactory())) @@ -1404,9 +1541,12 @@ private function dumpValue($value, $interpolate = true) if (null !== $this->definitionVariables && $this->definitionVariables->contains($value)) { return $this->dumpValue($this->definitionVariables->offsetGet($value), $interpolate); } - if (count($value->getMethodCalls()) > 0) { + if ($value->getMethodCalls()) { throw new RuntimeException('Cannot dump definitions which have method calls.'); } + if ($value->getOverriddenGetters()) { + throw new RuntimeException('Cannot dump definitions which have overridden getters.'); + } if (null !== $value->getConfigurator()) { throw new RuntimeException('Cannot dump definitions which have a configurator.'); } @@ -1755,7 +1895,7 @@ private function generateSignature(\ReflectionFunctionAbstract $r) } elseif (preg_match('/^(?:[^ ]++ ){4}([a-zA-Z_\x7F-\xFF][^ ]++)/', $p, $type)) { $type = $type[1]; } - if ($type && $type = $this->generateTypeHint($type, $r)) { + if ($type && $type = ContainerBuilder::generateTypeHint($type, $r)) { $k = $type.' '.$k; } if ($type && $p->allowsNull()) { @@ -1795,35 +1935,4 @@ private function generateCall(\ReflectionFunctionAbstract $r) return ($r->isClosure() ? '' : $r->name).'('.implode(', ', $call).')'; } - - private function generateTypeHint($type, \ReflectionFunctionAbstract $r) - { - if (is_string($type)) { - $name = $type; - - if ('callable' === $name || 'array' === $name) { - return $name; - } - } else { - $name = $type instanceof \ReflectionNamedType ? $type->getName() : $type->__toString(); - - if ($type->isBuiltin()) { - return $name; - } - } - $lcName = strtolower($name); - - if ('self' !== $lcName && 'parent' !== $lcName) { - return '\\'.$name; - } - if (!$r instanceof \ReflectionMethod) { - return; - } - if ('self' === $lcName) { - return '\\'.$r->getDeclaringClass()->name; - } - if ($parent = $r->getDeclaringClass()->getParentClass()) { - return '\\'.$parent->name; - } - } } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php index e067fb931d4b9..03097e3d137ff 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php @@ -167,6 +167,10 @@ private function addService($definition, $id, \DOMElement $parent) $this->convertParameters($parameters, 'property', $service, 'name'); } + if ($parameters = $definition->getOverriddenGetters()) { + $this->convertParameters($parameters, 'getter', $service, 'name'); + } + $this->addMethodCalls($definition->getMethodCalls(), $service); if ($callable = $definition->getFactory()) { diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php index b6c4e6639cc0e..87a6024506cec 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php @@ -126,6 +126,10 @@ private function addService($id, $definition) $code .= sprintf(" properties: %s\n", $this->dumper->dump($this->dumpValue($definition->getProperties()), 0)); } + if ($definition->getOverriddenGetters()) { + $code .= sprintf(" getters:\n%s\n", $this->dumper->dump($this->dumpValue($definition->getOverriddenGetters()), 0)); + } + if ($definition->getMethodCalls()) { $code .= sprintf(" calls:\n%s\n", $this->dumper->dump($this->dumpValue($definition->getMethodCalls()), 1, 12)); } diff --git a/src/Symfony/Component/DependencyInjection/LazyProxy/GetterProxyInterface.php b/src/Symfony/Component/DependencyInjection/LazyProxy/GetterProxyInterface.php new file mode 100644 index 0000000000000..1178ae59afc42 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/LazyProxy/GetterProxyInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\LazyProxy; + +/** + * Interface used to label proxy classes with overridden getters as generated while compiling the container. + * + * @author Nicolas Grekas + * + * @experimental in version 3.3 + */ +interface GetterProxyInterface +{ +} diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index d428c1399b871..240270139c0a2 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -244,6 +244,7 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults = $definition->setArguments($this->getArgumentsAsPhp($service, 'argument')); $definition->setProperties($this->getArgumentsAsPhp($service, 'property')); + $definition->setOverriddenGetters($this->getArgumentsAsPhp($service, 'getter')); if ($factories = $this->getChildren($service, 'factory')) { $factory = $factories[0]; @@ -385,7 +386,7 @@ private function processAnonymousServices(\DOMDocument $xml, $file) $xpath->registerNamespace('container', self::NS); // anonymous services as arguments/properties - if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]')) { + if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]|//container:getter[@type="service"][not(@id)]')) { foreach ($nodes as $node) { // give it a unique name $id = sprintf('%d_%s', ++$count, hash('sha256', $file)); diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index 48d0e92e44197..ba29ca60b50df 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -49,6 +49,7 @@ class YamlFileLoader extends FileLoader 'file' => 'file', 'arguments' => 'arguments', 'properties' => 'properties', + 'getters' => 'getters', 'configurator' => 'configurator', 'calls' => 'calls', 'tags' => 'tags', @@ -333,6 +334,10 @@ private function parseDefinition($id, $service, $file, array $defaults) $definition->setConfigurator($this->parseCallable($service['configurator'], 'configurator', $id, $file)); } + if (isset($service['getters'])) { + $definition->setOverriddenGetters($this->resolveServices($service['getters'])); + } + if (isset($service['calls'])) { if (!is_array($service['calls'])) { throw new InvalidArgumentException(sprintf('Parameter "calls" must be an array for service "%s" in %s. Check your YAML syntax.', $id, $file)); 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 02b0187b80c16..2a90dae6cf8d4 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 @@ -116,6 +116,7 @@ + @@ -171,6 +172,18 @@ + + + + + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInvalidReferencesPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInvalidReferencesPassTest.php index ecc5bee8e6078..b1ef9cc0e2c96 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInvalidReferencesPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInvalidReferencesPassTest.php @@ -107,6 +107,19 @@ public function testProcessRemovesPropertiesOnInvalid() $this->assertEquals(array(), $def->getProperties()); } + public function testProcessRemovesOverriddenGettersOnInvalid() + { + $container = new ContainerBuilder(); + $def = $container + ->register('foo') + ->setOverriddenGetter('foo', new Reference('bar', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)) + ; + + $this->process($container); + + $this->assertEquals(array(), $def->getOverriddenGetters()); + } + protected function process(ContainerBuilder $container) { $pass = new ResolveInvalidReferencesPass(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index e9284b24a6cf7..5398ea450ab58 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -797,6 +797,59 @@ public function testExtensionConfig() $this->assertEquals(array($second, $first), $configs); } + public function testOverriddenGetter() + { + $builder = new ContainerBuilder(); + $builder + ->register('foo', 'ReflectionClass') + ->addArgument('stdClass') + ->setOverriddenGetter('getName', 'bar'); + + $foo = $builder->get('foo'); + + $this->assertInstanceOf('ReflectionClass', $foo); + $this->assertSame('bar', $foo->getName()); + } + + public function testOverriddenGetterOnInvalid() + { + $builder = new ContainerBuilder(); + $builder + ->register('foo', 'ReflectionClass') + ->addArgument('stdClass') + ->setOverriddenGetter('getName', new Reference('bar', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)); + + $foo = $builder->get('foo'); + + $this->assertInstanceOf('ReflectionClass', $foo); + $this->assertSame('stdClass', $foo->getName()); + } + + /** + * @dataProvider provideBadOverridenGetters + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + */ + public function testBadOverridenGetters($expectedMessage, $getter, $id = 'foo') + { + $container = include __DIR__.'/Fixtures/containers/container30.php'; + $container->getDefinition($id)->setOverriddenGetter($getter, 123); + + $this->setExpectedException(RuntimeException::class, $expectedMessage); + $container->get($id); + } + + public function provideBadOverridenGetters() + { + yield array('Unable to configure getter injection for service "foo": method "Symfony\Component\DependencyInjection\Tests\Fixtures\Container30\Foo::getnotfound" does not exist.', 'getNotFound'); + yield array('Unable to configure getter injection for service "foo": method "Symfony\Component\DependencyInjection\Tests\Fixtures\Container30\Foo::getPrivate" must be public or protected.', 'getPrivate'); + yield array('Unable to configure getter injection for service "foo": method "Symfony\Component\DependencyInjection\Tests\Fixtures\Container30\Foo::getStatic" cannot be static.', 'getStatic'); + yield array('Unable to configure getter injection for service "foo": method "Symfony\Component\DependencyInjection\Tests\Fixtures\Container30\Foo::getFinal" cannot be marked as final.', 'getFinal'); + yield array('Unable to configure getter injection for service "foo": method "Symfony\Component\DependencyInjection\Tests\Fixtures\Container30\Foo::getRef" cannot return by reference.', 'getRef'); + yield array('Unable to configure getter injection for service "foo": method "Symfony\Component\DependencyInjection\Tests\Fixtures\Container30\Foo::getParam" cannot have any arguments.', 'getParam'); + yield array('Unable to configure getter injection for service "bar": class "Symfony\Component\DependencyInjection\Tests\Fixtures\Container30\Bar" cannot be marked as final.', 'getParam', 'bar'); + yield array('Cannot create service "baz": factories and overridden getters are incompatible with each other.', 'getParam', 'baz'); + } + public function testAbstractAlias() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 6efc2e9a3900c..7877a64436071 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -20,6 +20,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Variable; use Symfony\Component\ExpressionLanguage\Expression; @@ -311,6 +312,66 @@ public function testDumpAutowireData() $this->assertStringEqualsFile(self::$fixturesPath.'/php/services24.php', $dumper->dump()); } + public function testDumpOverridenGetters() + { + $container = include self::$fixturesPath.'/containers/container29.php'; + $container->compile(); + $container->getDefinition('foo') + ->setOverriddenGetter('getInvalid', array(new Reference('bar', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE))); + $dumper = new PhpDumper($container); + + $dump = $dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_Overriden_Getters')); + $this->assertStringEqualsFile(self::$fixturesPath.'/php/services29.php', $dump); + $res = $container->getResources(); + $this->assertSame(realpath(self::$fixturesPath.'/containers/container29.php'), array_pop($res)->getResource()); + + eval('?>'.$dump); + + $container = new \Symfony_DI_PhpDumper_Test_Overriden_Getters(); + + $foo = $container->get('foo'); + + $this->assertSame('public', $foo->getPublic()); + $this->assertSame('protected', $foo->getGetProtected()); + $this->assertSame($foo, $foo->getSelf()); + $this->assertSame(456, $foo->getInvalid()); + + $baz = $container->get('baz'); + $r = new \ReflectionMethod($baz, 'getBaz'); + $r->setAccessible(true); + + $this->assertTrue($r->isProtected()); + $this->assertSame('baz', $r->invoke($baz)); + } + + /** + * @dataProvider provideBadOverridenGetters + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + */ + public function testBadOverridenGetters($expectedMessage, $getter, $id = 'foo') + { + $container = include self::$fixturesPath.'/containers/container30.php'; + $container->getDefinition($id)->setOverriddenGetter($getter, 123); + + $container->compile(); + $dumper = new PhpDumper($container); + + $this->setExpectedException(RuntimeException::class, $expectedMessage); + $dumper->dump(); + } + + public function provideBadOverridenGetters() + { + yield array('Unable to configure getter injection for service "foo": method "Symfony\Component\DependencyInjection\Tests\Fixtures\Container30\Foo::getnotfound" does not exist.', 'getNotFound'); + yield array('Unable to configure getter injection for service "foo": method "Symfony\Component\DependencyInjection\Tests\Fixtures\Container30\Foo::getPrivate" must be public or protected.', 'getPrivate'); + yield array('Unable to configure getter injection for service "foo": method "Symfony\Component\DependencyInjection\Tests\Fixtures\Container30\Foo::getStatic" cannot be static.', 'getStatic'); + yield array('Unable to configure getter injection for service "foo": method "Symfony\Component\DependencyInjection\Tests\Fixtures\Container30\Foo::getFinal" cannot be marked as final.', 'getFinal'); + yield array('Unable to configure getter injection for service "foo": method "Symfony\Component\DependencyInjection\Tests\Fixtures\Container30\Foo::getRef" cannot return by reference.', 'getRef'); + yield array('Unable to configure getter injection for service "foo": method "Symfony\Component\DependencyInjection\Tests\Fixtures\Container30\Foo::getParam" cannot have any arguments.', 'getParam'); + yield array('Unable to configure getter injection for service "bar": class "Symfony\Component\DependencyInjection\Tests\Fixtures\Container30\Bar" cannot be marked as final.', 'getParam', 'bar'); + yield array('Cannot dump definition for service "baz": factories and overridden getters are incompatible with each other.', 'getParam', 'baz'); + } + public function testEnvParameter() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container29.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container29.php new file mode 100644 index 0000000000000..89fee82180b0f --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container29.php @@ -0,0 +1,57 @@ +getProtected(); + } + } + + class Baz + { + final public function __construct() + { + } + + protected function getBaz() + { + return 234; + } + } +} + +$container = new ContainerBuilder(); + +$container + ->register('foo', Foo::class) + ->setOverriddenGetter('getPublic', 'public') + ->setOverriddenGetter('getProtected', 'protected') + ->setOverriddenGetter('getSelf', new Reference('foo')) +; + +$container + ->register('baz', Baz::class) + ->setOverriddenGetter('getBaz', 'baz') +; + +return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container30.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container30.php new file mode 100644 index 0000000000000..5bff86c90542d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container30.php @@ -0,0 +1,42 @@ +register('foo', Foo::class); +$container->register('bar', Bar::class); +$container->register('baz', Bar::class)->setFactory('foo'); + +return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services29.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services29.php new file mode 100644 index 0000000000000..be8e6d48b7c5f --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services29.php @@ -0,0 +1,152 @@ +services = array(); + $this->methodMap = array( + 'baz' => 'getBazService', + 'foo' => 'getFooService', + ); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped frozen container.'); + } + + /** + * {@inheritdoc} + */ + public function isFrozen() + { + return true; + } + + /** + * Gets the 'baz' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Fixtures\Container29\Baz A Symfony\Component\DependencyInjection\Tests\Fixtures\Container29\Baz instance + */ + protected function getBazService() + { + return $this->services['baz'] = $this->instantiateProxy(SymfonyProxy_46eafd3003c2798ed583593e686cb95e::class, array(), true); + } + + /** + * Gets the 'foo' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Fixtures\Container29\Foo A Symfony\Component\DependencyInjection\Tests\Fixtures\Container29\Foo instance + */ + protected function getFooService() + { + return $this->services['foo'] = new SymfonyProxy_78f39120a5353f811849a5b3f3e6d70c($this); + } + + private function instantiateProxy($class, $args, $useConstructor) + { + static $reflectionCache; + + if (null === $r = &$reflectionCache[$class]) { + $r[0] = new \ReflectionClass($class); + $r[1] = $r[0]->getProperty('container6HqvH3fsTTC6dr66HyT2Jw'); + $r[1]->setAccessible(true); + $r[2] = $r[0]->getConstructor(); + } + $service = $useConstructor ? $r[0]->newInstanceWithoutConstructor() : $r[0]->newInstanceArgs($args); + $r[1]->setValue($service, $this); + if ($r[2] && $useConstructor) { + $r[2]->invokeArgs($service, $args); + } + + return $service; + } +} + +class SymfonyProxy_46eafd3003c2798ed583593e686cb95e extends \Symfony\Component\DependencyInjection\Tests\Fixtures\Container29\Baz implements \Symfony\Component\DependencyInjection\LazyProxy\GetterProxyInterface +{ + private $container6HqvH3fsTTC6dr66HyT2Jw; + private $getters6HqvH3fsTTC6dr66HyT2Jw; + + protected function getBaz() + { + return 'baz'; + } +} + +class SymfonyProxy_78f39120a5353f811849a5b3f3e6d70c extends \Symfony\Component\DependencyInjection\Tests\Fixtures\Container29\Foo implements \Symfony\Component\DependencyInjection\LazyProxy\GetterProxyInterface +{ + private $container6HqvH3fsTTC6dr66HyT2Jw; + private $getters6HqvH3fsTTC6dr66HyT2Jw; + + public function __construct($container6HqvH3fsTTC6dr66HyT2Jw) + { + $this->container6HqvH3fsTTC6dr66HyT2Jw = $container6HqvH3fsTTC6dr66HyT2Jw; + } + + public function getPublic() + { + return 'public'; + } + + protected function getProtected() + { + return 'protected'; + } + + public function getSelf() + { + if (null === $g = &$this->getters6HqvH3fsTTC6dr66HyT2Jw[__FUNCTION__]) { + $g = \Closure::bind(function () { return ${($_ = isset($this->services['foo']) ? $this->services['foo'] : $this->get('foo')) && false ?: '_'}; }, $this->container6HqvH3fsTTC6dr66HyT2Jw, $this->container6HqvH3fsTTC6dr66HyT2Jw); + } + + return $g(); + } + + public function getInvalid() + { + if (null === $g = &$this->getters6HqvH3fsTTC6dr66HyT2Jw[__FUNCTION__]) { + $g = \Closure::bind(function () { return array(0 => $this->get('bar', ContainerInterface::NULL_ON_INVALID_REFERENCE)); }, $this->container6HqvH3fsTTC6dr66HyT2Jw, $this->container6HqvH3fsTTC6dr66HyT2Jw); + } + + if ($this->container6HqvH3fsTTC6dr66HyT2Jw->has('bar')) { + return $g(); + } + + return parent::getInvalid(); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services31.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services31.xml new file mode 100644 index 0000000000000..b443ca761e60c --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services31.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services31.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services31.yml new file mode 100644 index 0000000000000..4b0bfafc6b1fe --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services31.yml @@ -0,0 +1,6 @@ +services: + foo: + class: Foo + getters: + getBar: { bar: '@bar' } + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index a1b95616805e4..4fcd667f578d1 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -576,6 +576,15 @@ public function testAutowire() $this->assertEquals(array('set*', 'bar'), $container->getDefinition('autowire_array')->getAutowiredMethods()); } + public function testGetter() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services31.xml'); + + $this->assertEquals(array('getbar' => array('bar' => new Reference('bar'))), $container->getDefinition('foo')->getOverriddenGetters()); + } + /** * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException */ diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 1646c9986c800..c36418be9fe27 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -405,6 +405,15 @@ public function testDefaults() $this->assertFalse($container->getDefinition('no_defaults_child')->isAutowired()); } + public function testGetter() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services31.yml'); + + $this->assertEquals(array('getbar' => array('bar' => new Reference('bar'))), $container->getDefinition('foo')->getOverriddenGetters()); + } + /** * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException * @expectedExceptionMessage The value of the "decorates" option for the "bar" service must be the id of the service without the "@" prefix (replace "@foo" with "foo"). diff --git a/src/Symfony/Component/VarDumper/Caster/ClassStub.php b/src/Symfony/Component/VarDumper/Caster/ClassStub.php index 2b3e9dbd2dcaf..3739a4f2eec89 100644 --- a/src/Symfony/Component/VarDumper/Caster/ClassStub.php +++ b/src/Symfony/Component/VarDumper/Caster/ClassStub.php @@ -11,6 +11,8 @@ namespace Symfony\Component\VarDumper\Caster; +use Symfony\Component\DependencyInjection\LazyProxy\GetterProxyInterface; + /** * Represents a PHP class identifier. * @@ -66,6 +68,18 @@ public function __construct($identifier, $callable = null) return; } + if (interface_exists(GetterProxyInterface::class, false)) { + $c = $r instanceof \ReflectionMethod ? $r->getDeclaringClass() : $r; + if ($c instanceof \ReflectionClass && $c->implementsInterface(GetterProxyInterface::class)) { + $p = $c->getParentClass(); + $this->value = $identifier = str_replace($c->name, $p->name.'@proxy', $identifier); + if (0 < $i = strrpos($identifier, '\\')) { + $this->attr['ellipsis'] = strlen($identifier) - $i; + } + $r = $r instanceof \ReflectionMethod ? $p->getMethod($r->name) : $p; + } + } + if ($f = $r->getFileName()) { $this->attr['file'] = $f; $this->attr['line'] = $r->getStartLine(); diff --git a/src/Symfony/Component/VarDumper/Caster/SymfonyCaster.php b/src/Symfony/Component/VarDumper/Caster/SymfonyCaster.php index 5d9decb67aeb3..485c86ff10781 100644 --- a/src/Symfony/Component/VarDumper/Caster/SymfonyCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/SymfonyCaster.php @@ -11,6 +11,7 @@ namespace Symfony\Component\VarDumper\Caster; +use Symfony\Component\DependencyInjection\LazyProxy\GetterProxyInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\VarDumper\Cloner\Stub; @@ -40,4 +41,19 @@ public static function castRequest(Request $request, array $a, Stub $stub, $isNe return $a; } + + public static function castGetterProxy(GetterProxyInterface $proxy, array $a, Stub $stub, $isNested) + { + $privatePrefix = sprintf("\0%s\0", $stub->class); + $stub->class = get_parent_class($proxy).'@proxy'; + + foreach ($a as $k => $v) { + if ("\0" === $k[0] && 0 === strpos($k, $privatePrefix)) { + ++$stub->cut; + unset($a[$k]); + } + } + + return $a; + } } diff --git a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php index 5ef1dd9c9d45b..3225c0056c009 100644 --- a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php +++ b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php @@ -75,6 +75,7 @@ abstract class AbstractCloner implements ClonerInterface 'Exception' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castException', 'Error' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castError', 'Symfony\Component\DependencyInjection\ContainerInterface' => 'Symfony\Component\VarDumper\Caster\StubCaster::cutInternals', + 'Symfony\Component\DependencyInjection\LazyProxy\GetterProxyInterface' => 'Symfony\Component\VarDumper\Caster\SymfonyCaster::castGetterProxy', 'Symfony\Component\HttpFoundation\Request' => 'Symfony\Component\VarDumper\Caster\SymfonyCaster::castRequest', 'Symfony\Component\VarDumper\Exception\ThrowingCasterException' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castThrowingCasterException', 'Symfony\Component\VarDumper\Caster\TraceStub' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castTraceStub',