Permalink
Browse files

feature #21031 [DI] Getter autowiring (dunglas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI] Getter autowiring

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

This PR adds support for getter autowiring. #20973 must be merged first.

Example:

```yaml
# app/config/config.yml

services:
    Foo\Bar:
        autowire: ['get*']
```

```php
namespace Foo;

class Bar
{
    protected function getBaz(): Baz // this feature only works with PHP 7+
    {
    }
}

class Baz
{
}
````

`Baz` will be automatically registered as a service and an instance will be returned when `Bar::getBaz` will be called (and only at this time, lazy loading).

This feature requires PHP 7 or superior.

Commits
-------

c48c36b [DI] Add support for getter autowiring
  • Loading branch information...
2 parents 03b7cf7 + c48c36b commit b50efa5006207c01262f019925ba301e1f1e7ead @fabpot fabpot committed Feb 2, 2017
@@ -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
@@ -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,15 +182,15 @@ 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;
}
}
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) {
@@ -299,6 +304,55 @@ private function autowireMethod(\ReflectionMethod $reflectionMethod, array $argu
}
/**
+ * 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.
*/
private function populateAvailableTypes()
@@ -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 <dunglas@gmail.com>
@@ -517,6 +518,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
@@ -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');
@@ -0,0 +1,65 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\DependencyInjection\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 <dunglas@gmail.com>
+ */
+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
+ }
+}

0 comments on commit b50efa5

Please sign in to comment.