Skip to content

Commit bee5364

Browse files
committed
[DependencyInjection] Automatically detect the definitions class when possible
1 parent 56c7206 commit bee5364

File tree

4 files changed

+237
-0
lines changed

4 files changed

+237
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\Definition;
16+
use Symfony\Component\DependencyInjection\Reference;
17+
18+
/**
19+
* @author Guilhem N. <egetick@gmail.com>
20+
*/
21+
class FactoryReturnTypePass implements CompilerPassInterface
22+
{
23+
/**
24+
* {@inheritdoc}
25+
*/
26+
public function process(ContainerBuilder $container)
27+
{
28+
// works only since php 7.0 and hhvm 3.11
29+
if (!method_exists(\ReflectionMethod::class, 'getReturnType')) {
30+
return;
31+
}
32+
33+
foreach ($container->getDefinitions() as $id => $definition) {
34+
$this->updateDefinition($container, $id, $definition);
35+
}
36+
}
37+
38+
private function updateDefinition(ContainerBuilder $container, $id, Definition $definition, array $previous = array())
39+
{
40+
// circular reference
41+
if (isset($previous[$id])) {
42+
return;
43+
}
44+
45+
$factory = $definition->getFactory();
46+
if (null === $factory || null !== $definition->getClass()) {
47+
return;
48+
}
49+
50+
$class = null;
51+
if (is_string($factory)) {
52+
try {
53+
$m = new \ReflectionFunction($factory);
54+
} catch (\ReflectionException $e) {
55+
return;
56+
}
57+
} else {
58+
if ($factory[0] instanceof Reference) {
59+
$previous[$id] = true;
60+
$factoryDefinition = $container->findDefinition((string) $factory[0]);
61+
$this->updateDefinition($container, (string) $factory[0], $factoryDefinition, $previous);
62+
$class = $factoryDefinition->getClass();
63+
} else {
64+
$class = $factory[0];
65+
}
66+
67+
try {
68+
$m = new \ReflectionMethod($class, $factory[1]);
69+
} catch (\ReflectionException $e) {
70+
return;
71+
}
72+
}
73+
74+
$returnType = $m->getReturnType();
75+
if (null !== $returnType && !$returnType->isBuiltin()) {
76+
$returnType = (string) $returnType;
77+
if (null !== $class) {
78+
$declaringClass = $m->getDeclaringClass()->getName();
79+
if ('self' === $returnType) {
80+
$returnType = $declaringClass;
81+
} elseif ('parent' === $returnType) {
82+
$returnType = get_parent_class($declaringClass) ?: null;
83+
}
84+
}
85+
86+
$definition->setClass($returnType);
87+
}
88+
}
89+
}

src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public function __construct()
4747
new CheckDefinitionValidityPass(),
4848
new ResolveReferencesToAliasesPass(),
4949
new ResolveInvalidReferencesPass(),
50+
new FactoryReturnTypePass(),
5051
new AutowirePass(),
5152
new AnalyzeServiceReferencesPass(true),
5253
new CheckCircularReferencesPass(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\FactoryReturnTypePass;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Reference;
17+
use Symfony\Component\DependencyInjection\Tests\Fixtures\factoryFunction;
18+
use Symfony\Component\DependencyInjection\Tests\Fixtures\FactoryDummy;
19+
use Symfony\Component\DependencyInjection\Tests\Fixtures\FactoryParent;
20+
21+
/**
22+
* @author Guilhem N. <egetick@gmail.com>
23+
*/
24+
class FactoryReturnTypePassTest extends \PHPUnit_Framework_TestCase
25+
{
26+
public function testProcess()
27+
{
28+
$container = new ContainerBuilder();
29+
30+
$factory = $container->register('factory');
31+
$factory->setFactory(array(FactoryDummy::class, 'createFactory'));
32+
33+
$foo = $container->register('foo');
34+
$foo->setFactory(array(new Reference('factory'), 'create'));
35+
36+
$bar = $container->register('bar', __CLASS__);
37+
$bar->setFactory(array(new Reference('factory'), 'create'));
38+
39+
$pass = new FactoryReturnTypePass();
40+
$pass->process($container);
41+
42+
if (method_exists(\ReflectionMethod::class, 'getReturnType')) {
43+
$this->assertEquals(FactoryDummy::class, $factory->getClass());
44+
$this->assertEquals(\stdClass::class, $foo->getClass());
45+
} else {
46+
$this->assertNull($factory->getClass());
47+
$this->assertNull($foo->getClass());
48+
}
49+
$this->assertEquals(__CLASS__, $bar->getClass());
50+
}
51+
52+
/**
53+
* @dataProvider returnTypesProvider
54+
*/
55+
public function testReturnTypes($factory, $returnType, $hhvmSupport = true)
56+
{
57+
if (!$hhvmSupport && defined('HHVM_VERSION')) {
58+
$this->markTestSkipped('Scalar typehints not supported by hhvm.');
59+
}
60+
61+
$container = new ContainerBuilder();
62+
63+
$service = $container->register('service');
64+
$service->setFactory($factory);
65+
66+
$pass = new FactoryReturnTypePass();
67+
$pass->process($container);
68+
69+
if (method_exists(\ReflectionMethod::class, 'getReturnType')) {
70+
$this->assertEquals($returnType, $service->getClass());
71+
} else {
72+
$this->assertNull($service->getClass());
73+
}
74+
}
75+
76+
public function returnTypesProvider()
77+
{
78+
return array(
79+
// must be loaded before the function as they are in the same file
80+
array(array(FactoryDummy::class, 'createBuiltin'), null, false),
81+
array(array(FactoryDummy::class, 'createParent'), FactoryParent::class),
82+
array(array(FactoryDummy::class, 'createSelf'), FactoryDummy::class),
83+
array(factoryFunction::class, FactoryDummy::class),
84+
);
85+
}
86+
87+
public function testCircularReference()
88+
{
89+
$container = new ContainerBuilder();
90+
91+
$factory = $container->register('factory');
92+
$factory->setFactory(array(new Reference('factory2'), 'createSelf'));
93+
94+
$factory2 = $container->register('factory2');
95+
$factory2->setFactory(array(new Reference('factory'), 'create'));
96+
97+
$pass = new FactoryReturnTypePass();
98+
$pass->process($container);
99+
100+
$this->assertNull($factory->getClass());
101+
$this->assertNull($factory2->getClass());
102+
}
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
13+
14+
class FactoryDummy extends FactoryParent
15+
{
16+
public static function createFactory(): FactoryDummy
17+
{
18+
}
19+
20+
public function create(): \stdClass
21+
{
22+
}
23+
24+
// Not supported by hhvm
25+
public function createBuiltin(): int
26+
{
27+
}
28+
29+
public static function createSelf(): self
30+
{
31+
}
32+
33+
public static function createParent(): parent
34+
{
35+
}
36+
}
37+
38+
class FactoryParent
39+
{
40+
}
41+
42+
function factoryFunction(): FactoryDummy
43+
{
44+
}

0 commit comments

Comments
 (0)