From c48c36be8fe69e73346ceae53d83de924dddcb6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Fri, 23 Dec 2016 13:58:16 +0100 Subject: [PATCH] [DI] Add support for getter autowiring --- .../DependencyInjection/CHANGELOG.md | 1 + .../Compiler/AutowirePass.php | 68 +++++++++++++++++-- .../Tests/Compiler/AutowirePassTest.php | 31 +++++++++ .../Tests/Dumper/PhpDumperTest.php | 4 +- .../Tests/Fixtures/GetterOverriding.php | 65 ++++++++++++++++++ 5 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/GetterOverriding.php diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 947a6f3de879..e1aff752e153 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * deprecated `ContainerBuilder::getClassResource()`, use `ContainerBuilder::getReflectionClass()` or `ContainerBuilder::addObjectResource()` instead * added `ContainerBuilder::fileExists()` for checking and tracking file or directory existence * deprecated autowiring-types, use aliases instead + * [EXPERIMENTAL] added support for getter autowiring * [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 diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index a5765f4a6e72..65d49fc8c057 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -36,7 +36,7 @@ public function process(ContainerBuilder $container) try { parent::process($container); } finally { - // Free memory and remove circular reference to container + // Free memory $this->definedTypes = array(); $this->types = null; $this->ambiguousServiceTypes = array(); @@ -90,6 +90,7 @@ protected function processValue($value, $isRoot = false) } $methodCalls = $this->autowireMethodCalls($reflectionClass, $methodCalls, $autowiredMethods); + $overriddenGetters = $this->autowireOverridenGetters($value->getOverriddenGetters(), $autowiredMethods); if ($constructor) { list(, $arguments) = array_shift($methodCalls); @@ -103,6 +104,10 @@ protected function processValue($value, $isRoot = false) $value->setMethodCalls($methodCalls); } + if ($overriddenGetters !== $value->getOverriddenGetters()) { + $value->setOverriddenGetters($overriddenGetters); + } + return parent::processValue($value, $isRoot); } @@ -124,7 +129,7 @@ private function getMethodsToAutowire(\ReflectionClass $reflectionClass, array $ $regexList[] = '/^'.str_replace('\*', '.*', preg_quote($pattern, '/')).'$/i'; } - foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) { + foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $reflectionMethod) { if ($reflectionMethod->isStatic()) { continue; } @@ -164,7 +169,7 @@ private function autowireMethodCalls(\ReflectionClass $reflectionClass, array $m list($method, $arguments) = $call; $method = $parameterBag->resolveValue($method); - if (isset($autowiredMethods[$lcMethod = strtolower($method)])) { + if (isset($autowiredMethods[$lcMethod = strtolower($method)]) && $autowiredMethods[$lcMethod]->isPublic()) { $reflectionMethod = $autowiredMethods[$lcMethod]; unset($autowiredMethods[$lcMethod]); } else { @@ -177,7 +182,7 @@ private function autowireMethodCalls(\ReflectionClass $reflectionClass, array $m } } - $arguments = $this->autowireMethod($reflectionMethod, $arguments, true); + $arguments = $this->autowireMethodCall($reflectionMethod, $arguments, true); if ($arguments !== $call[1]) { $methodCalls[$i][1] = $arguments; @@ -185,7 +190,7 @@ private function autowireMethodCalls(\ReflectionClass $reflectionClass, array $m } foreach ($autowiredMethods as $reflectionMethod) { - if ($arguments = $this->autowireMethod($reflectionMethod, array(), false)) { + if ($reflectionMethod->isPublic() && $arguments = $this->autowireMethodCall($reflectionMethod, array(), false)) { $methodCalls[] = array($reflectionMethod->name, $arguments); } } @@ -194,7 +199,7 @@ private function autowireMethodCalls(\ReflectionClass $reflectionClass, array $m } /** - * Autowires the constructor or a setter. + * Autowires the constructor or a method. * * @param \ReflectionMethod $reflectionMethod * @param array $arguments @@ -204,7 +209,7 @@ private function autowireMethodCalls(\ReflectionClass $reflectionClass, array $m * * @throws RuntimeException */ - private function autowireMethod(\ReflectionMethod $reflectionMethod, array $arguments, $mustAutowire) + private function autowireMethodCall(\ReflectionMethod $reflectionMethod, array $arguments, $mustAutowire) { $didAutowire = false; // Whether any arguments have been autowired or not foreach ($reflectionMethod->getParameters() as $index => $parameter) { @@ -298,6 +303,55 @@ private function autowireMethod(\ReflectionMethod $reflectionMethod, array $argu return $arguments; } + /** + * Autowires getters. + * + * @param array $overridenGetters + * @param array $autowiredMethods + * + * @return array + */ + private function autowireOverridenGetters(array $overridenGetters, array $autowiredMethods) + { + foreach ($autowiredMethods as $reflectionMethod) { + if (isset($overridenGetters[strtolower($reflectionMethod->name)]) + || !method_exists($reflectionMethod, 'getReturnType') + || 0 !== $reflectionMethod->getNumberOfParameters() + || $reflectionMethod->isFinal() + || $reflectionMethod->returnsReference() + || !$returnType = $reflectionMethod->getReturnType() + ) { + continue; + } + $typeName = $returnType instanceof \ReflectionNamedType ? $returnType->getName() : $returnType->__toString(); + + if ($this->container->has($typeName) && !$this->container->findDefinition($typeName)->isAbstract()) { + $overridenGetters[$reflectionMethod->name] = new Reference($typeName); + continue; + } + + if (null === $this->types) { + $this->populateAvailableTypes(); + } + + if (isset($this->types[$typeName])) { + $value = new Reference($this->types[$typeName]); + } elseif ($returnType = $this->container->getReflectionClass($typeName, true)) { + try { + $value = $this->createAutowiredDefinition($returnType); + } catch (RuntimeException $e) { + continue; + } + } else { + continue; + } + + $overridenGetters[$reflectionMethod->name] = $value; + } + + return $overridenGetters; + } + /** * Populates the list of available types. */ diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index c8a98afa355e..76342b9347ff 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\FooVariadic; +use Symfony\Component\DependencyInjection\Tests\Fixtures\GetterOverriding; /** * @author Kévin Dunglas @@ -516,6 +517,31 @@ public function testExplicitMethodInjection() ); } + /** + * @requires PHP 7.1 + */ + public function testGetterOverriding() + { + $container = new ContainerBuilder(); + $container->register('b', B::class); + + $container + ->register('getter_overriding', GetterOverriding::class) + ->setOverriddenGetter('getExplicitlyDefined', new Reference('b')) + ->setAutowiredMethods(array('get*')) + ; + + $pass = new AutowirePass(); + $pass->process($container); + + $overridenGetters = $container->getDefinition('getter_overriding')->getOverriddenGetters(); + $this->assertEquals(array( + 'getexplicitlydefined' => new Reference('b'), + 'getfoo' => new Reference('autowired.Symfony\Component\DependencyInjection\Tests\Compiler\Foo'), + 'getbar' => new Reference('autowired.Symfony\Component\DependencyInjection\Tests\Compiler\Bar'), + ), $overridenGetters); + } + /** * @dataProvider getCreateResourceTests * @group legacy @@ -854,6 +880,11 @@ public function notASetter(A $a) { // should be called only when explicitly specified } + + protected function setProtectedMethod(A $a) + { + // should not be called + } } class SetterInjectionCollision diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 1fcd7da78122..bb8dfbb68bd6 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -354,8 +354,8 @@ public function testDumpOverridenGettersWithConstructor() $dump = $dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_Overriden_Getters_With_Constructor')); $this->assertStringEqualsFile(self::$fixturesPath.'/php/services_dump_overriden_getters_with_constructor.php', $dump); - $resources = array_map('strval', $container->getResources()); - $this->assertContains(realpath(self::$fixturesPath.'/containers/container_dump_overriden_getters_with_constructor.php'), $resources); + $res = $container->getResources(); + $this->assertSame('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Container34\Foo', (string) array_pop($res)); $baz = $container->get('baz'); $r = new \ReflectionMethod($baz, 'getBaz'); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/GetterOverriding.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/GetterOverriding.php new file mode 100644 index 000000000000..746f347c4ef9 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/GetterOverriding.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Symfony\Component\DependencyInjection\Tests\Compiler\A; +use Symfony\Component\DependencyInjection\Tests\Compiler\B; +use Symfony\Component\DependencyInjection\Tests\Compiler\Bar; +use Symfony\Component\DependencyInjection\Tests\Compiler\Foo; + +/** + * To test getter autowiring with PHP >= 7.1. + * + * @author Kévin Dunglas + */ +class GetterOverriding +{ + public function getFoo(): ?Foo + { + // should be called + } + + protected function getBar(): Bar + { + // should be called + } + + public function getNoTypeHint() + { + // should not be called + } + + public function getUnknown(): NotExist + { + // should not be called + } + + public function getExplicitlyDefined(): B + { + // should be called but not autowired + } + + public function getScalar(): string + { + // should not be called + } + + final public function getFinal(): A + { + // should not be called + } + + public function &getReference(): A + { + // should not be called + } +}