feature #19191 [DependencyInjection] Automatically detect the definitions class when possible (Ener-Getick)

This PR was merged into the 3.2-dev branch.

Discussion
----------

[DependencyInjection] Automatically detect the definitions class when possible

| Q             | A
| ------------- | ---
| Branch?       | "master"
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | https://github.com/symfony/symfony/issues/19161
| License       | MIT
| Doc PR        |

> Thanks to the features of php 7.0 we can now guess the class of a service created with a factory:
> ```php
> function myFactory(): MyServiceClass
> {
> }
> ```
>
> So I propose to create a new pass to automatically update the services definition when possible. This is particularly useful for autowiring (this way you don't have to copy-paste the class name of the service, especially when this is from a third party library).
>
> What do you think ?

Commits
-------

63afe3c [DependencyInjection] Automatically detect the definitions class when possible
This commit is contained in:
Fabien Potencier 2016-09-19 12:25:58 -07:00
commit 36c399f632
4 changed files with 237 additions and 0 deletions

View File

@ -0,0 +1,89 @@
<?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\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Guilhem N. <egetick@gmail.com>
*/
class FactoryReturnTypePass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
// works only since php 7.0 and hhvm 3.11
if (!method_exists(\ReflectionMethod::class, 'getReturnType')) {
return;
}
foreach ($container->getDefinitions() as $id => $definition) {
$this->updateDefinition($container, $id, $definition);
}
}
private function updateDefinition(ContainerBuilder $container, $id, Definition $definition, array $previous = array())
{
// circular reference
if (isset($previous[$id])) {
return;
}
$factory = $definition->getFactory();
if (null === $factory || null !== $definition->getClass()) {
return;
}
$class = null;
if (is_string($factory)) {
try {
$m = new \ReflectionFunction($factory);
} catch (\ReflectionException $e) {
return;
}
} else {
if ($factory[0] instanceof Reference) {
$previous[$id] = true;
$factoryDefinition = $container->findDefinition((string) $factory[0]);
$this->updateDefinition($container, (string) $factory[0], $factoryDefinition, $previous);
$class = $factoryDefinition->getClass();
} else {
$class = $factory[0];
}
try {
$m = new \ReflectionMethod($class, $factory[1]);
} catch (\ReflectionException $e) {
return;
}
}
$returnType = $m->getReturnType();
if (null !== $returnType && !$returnType->isBuiltin()) {
$returnType = (string) $returnType;
if (null !== $class) {
$declaringClass = $m->getDeclaringClass()->getName();
if ('self' === $returnType) {
$returnType = $declaringClass;
} elseif ('parent' === $returnType) {
$returnType = get_parent_class($declaringClass) ?: null;
}
}
$definition->setClass($returnType);
}
}
}

View File

@ -47,6 +47,7 @@ class PassConfig
new CheckDefinitionValidityPass(),
new ResolveReferencesToAliasesPass(),
new ResolveInvalidReferencesPass(),
new FactoryReturnTypePass(),
new AutowirePass(),
new AnalyzeServiceReferencesPass(true),
new CheckCircularReferencesPass(),

View File

@ -0,0 +1,103 @@
<?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\Compiler;
use Symfony\Component\DependencyInjection\Compiler\FactoryReturnTypePass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\factoryFunction;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FactoryDummy;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FactoryParent;
/**
* @author Guilhem N. <egetick@gmail.com>
*/
class FactoryReturnTypePassTest extends \PHPUnit_Framework_TestCase
{
public function testProcess()
{
$container = new ContainerBuilder();
$factory = $container->register('factory');
$factory->setFactory(array(FactoryDummy::class, 'createFactory'));
$foo = $container->register('foo');
$foo->setFactory(array(new Reference('factory'), 'create'));
$bar = $container->register('bar', __CLASS__);
$bar->setFactory(array(new Reference('factory'), 'create'));
$pass = new FactoryReturnTypePass();
$pass->process($container);
if (method_exists(\ReflectionMethod::class, 'getReturnType')) {
$this->assertEquals(FactoryDummy::class, $factory->getClass());
$this->assertEquals(\stdClass::class, $foo->getClass());
} else {
$this->assertNull($factory->getClass());
$this->assertNull($foo->getClass());
}
$this->assertEquals(__CLASS__, $bar->getClass());
}
/**
* @dataProvider returnTypesProvider
*/
public function testReturnTypes($factory, $returnType, $hhvmSupport = true)
{
if (!$hhvmSupport && defined('HHVM_VERSION')) {
$this->markTestSkipped('Scalar typehints not supported by hhvm.');
}
$container = new ContainerBuilder();
$service = $container->register('service');
$service->setFactory($factory);
$pass = new FactoryReturnTypePass();
$pass->process($container);
if (method_exists(\ReflectionMethod::class, 'getReturnType')) {
$this->assertEquals($returnType, $service->getClass());
} else {
$this->assertNull($service->getClass());
}
}
public function returnTypesProvider()
{
return array(
// must be loaded before the function as they are in the same file
array(array(FactoryDummy::class, 'createBuiltin'), null, false),
array(array(FactoryDummy::class, 'createParent'), FactoryParent::class),
array(array(FactoryDummy::class, 'createSelf'), FactoryDummy::class),
array(factoryFunction::class, FactoryDummy::class),
);
}
public function testCircularReference()
{
$container = new ContainerBuilder();
$factory = $container->register('factory');
$factory->setFactory(array(new Reference('factory2'), 'createSelf'));
$factory2 = $container->register('factory2');
$factory2->setFactory(array(new Reference('factory'), 'create'));
$pass = new FactoryReturnTypePass();
$pass->process($container);
$this->assertNull($factory->getClass());
$this->assertNull($factory2->getClass());
}
}

View File

@ -0,0 +1,44 @@
<?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;
class FactoryDummy extends FactoryParent
{
public static function createFactory(): FactoryDummy
{
}
public function create(): \stdClass
{
}
// Not supported by hhvm
public function createBuiltin(): int
{
}
public static function createSelf(): self
{
}
public static function createParent(): parent
{
}
}
class FactoryParent
{
}
function factoryFunction(): FactoryDummy
{
}