From 63afe3cd262cb28d5319295e3f1862bebf29464f Mon Sep 17 00:00:00 2001 From: Ener-Getick Date: Thu, 23 Jun 2016 22:16:26 +0200 Subject: [PATCH] [DependencyInjection] Automatically detect the definitions class when possible --- .../Compiler/FactoryReturnTypePass.php | 89 +++++++++++++++ .../Compiler/PassConfig.php | 1 + .../Compiler/FactoryReturnTypePassTest.php | 103 ++++++++++++++++++ .../Tests/Fixtures/FactoryDummy.php | 44 ++++++++ 4 files changed, 237 insertions(+) create mode 100644 src/Symfony/Component/DependencyInjection/Compiler/FactoryReturnTypePass.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Compiler/FactoryReturnTypePassTest.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/FactoryDummy.php diff --git a/src/Symfony/Component/DependencyInjection/Compiler/FactoryReturnTypePass.php b/src/Symfony/Component/DependencyInjection/Compiler/FactoryReturnTypePass.php new file mode 100644 index 0000000000000..66ddd03c99ba4 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/FactoryReturnTypePass.php @@ -0,0 +1,89 @@ + + * + * 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. + */ +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); + } + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index f33e9fa0450ce..71f2b3049c766 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -47,6 +47,7 @@ public function __construct() new CheckDefinitionValidityPass(), new ResolveReferencesToAliasesPass(), new ResolveInvalidReferencesPass(), + new FactoryReturnTypePass(), new AutowirePass(), new AnalyzeServiceReferencesPass(true), new CheckCircularReferencesPass(), diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/FactoryReturnTypePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/FactoryReturnTypePassTest.php new file mode 100644 index 0000000000000..ae91a7975cf8b --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/FactoryReturnTypePassTest.php @@ -0,0 +1,103 @@ + + * + * 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. + */ +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()); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/FactoryDummy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/FactoryDummy.php new file mode 100644 index 0000000000000..da984b562a39d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/FactoryDummy.php @@ -0,0 +1,44 @@ + + * + * 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 +{ +}