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
-------

c48c36be8f [DI] Add support for getter autowiring
This commit is contained in:
Fabien Potencier 2017-02-02 11:39:35 -08:00
commit b50efa5006
5 changed files with 160 additions and 9 deletions

View File

@ -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

View File

@ -36,7 +36,7 @@ class AutowirePass extends AbstractRecursivePass
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 @@ class AutowirePass extends AbstractRecursivePass
}
$methodCalls = $this->autowireMethodCalls($reflectionClass, $methodCalls, $autowiredMethods);
$overriddenGetters = $this->autowireOverridenGetters($value->getOverriddenGetters(), $autowiredMethods);
if ($constructor) {
list(, $arguments) = array_shift($methodCalls);
@ -103,6 +104,10 @@ class AutowirePass extends AbstractRecursivePass
$value->setMethodCalls($methodCalls);
}
if ($overriddenGetters !== $value->getOverriddenGetters()) {
$value->setOverriddenGetters($overriddenGetters);
}
return parent::processValue($value, $isRoot);
}
@ -124,7 +129,7 @@ class AutowirePass extends AbstractRecursivePass
$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 @@ class AutowirePass extends AbstractRecursivePass
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 @@ class AutowirePass extends AbstractRecursivePass
}
}
$arguments = $this->autowireMethod($reflectionMethod, $arguments, true);
$arguments = $this->autowireMethodCall($reflectionMethod, $arguments, true);
if ($arguments !== $call[1]) {
$methodCalls[$i][1] = $arguments;
@ -185,7 +190,7 @@ class AutowirePass extends AbstractRecursivePass
}
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 @@ class AutowirePass extends AbstractRecursivePass
}
/**
* Autowires the constructor or a setter.
* Autowires the constructor or a method.
*
* @param \ReflectionMethod $reflectionMethod
* @param array $arguments
@ -204,7 +209,7 @@ class AutowirePass extends AbstractRecursivePass
*
* @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 @@ class AutowirePass extends AbstractRecursivePass
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.
*/

View File

@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Compiler\AutowirePass;
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>
@ -516,6 +517,31 @@ class AutowirePassTest extends \PHPUnit_Framework_TestCase
);
}
/**
* @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 @@ class SetterInjection
{
// should be called only when explicitly specified
}
protected function setProtectedMethod(A $a)
{
// should not be called
}
}
class SetterInjectionCollision

View File

@ -354,8 +354,8 @@ class PhpDumperTest extends \PHPUnit_Framework_TestCase
$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');

View File

@ -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
}
}