From fecb2320a8cbe72339a12d204590a7a184b082a3 Mon Sep 17 00:00:00 2001 From: Bryan Davis Date: Sat, 24 Oct 2015 12:42:07 -0600 Subject: [PATCH] ObjectFactory: avoid using ReflectionClass Add a new ObjectFactory::constructClassInstance() method that uses a loop unrolling type of technique to avoid using ReflectionClass when creating new class instances with 10 or fewer constructor arguments. I really wanted to also include the use of PHP 5.6's `...` splat operator when supported but there is no way to conditionally use a new operator in a way that still allows older versions of PHP to parse the same source file. Bug: T115729 Change-Id: Ia29c4526f4bac51696654c9b0677cb3f70359966 --- includes/libs/ObjectFactory.php | 92 +++++++++++++++++-- .../includes/libs/ObjectFactoryTest.php | 29 ++++++ 2 files changed, 112 insertions(+), 9 deletions(-) diff --git a/includes/libs/ObjectFactory.php b/includes/libs/ObjectFactory.php index 0b9aa7ca60..61916120fd 100644 --- a/includes/libs/ObjectFactory.php +++ b/includes/libs/ObjectFactory.php @@ -35,13 +35,6 @@ class ObjectFactory { * an 'args' key that provides arguments to pass to the * constructor/callable. * - * Object construction using a specification having both 'class' and - * 'args' members will call the constructor of the class using - * ReflectionClass::newInstanceArgs. The use of ReflectionClass carries - * a performance penalty and should not be used to create large numbers of - * objects. If this is needed, consider introducing a factory method that - * can be called via call_user_func_array() instead. - * * Values in the arguments collection which are Closure instances will be * expanded by invoking them with no arguments before passing the * resulting value on to the constructor/callable. This can be used to @@ -77,8 +70,7 @@ class ObjectFactory { if ( !$args ) { $obj = new $clazz(); } else { - $ref = new ReflectionClass( $clazz ); - $obj = $ref->newInstanceArgs( $args ); + $obj = static::constructClassInstance( $clazz, $args ); } } elseif ( isset( $spec['factory'] ) ) { $obj = call_user_func_array( $spec['factory'], $args ); @@ -117,4 +109,86 @@ class ObjectFactory { } }, $list ); } + + /** + * Construct an instance of the given class using the given arguments. + * + * PHP's `call_user_func_array()` doesn't work with object construction so + * we have to use other measures. Starting with PHP 5.6.0 we could use the + * "splat" operator (`...`) to unpack the array into an argument list. + * Sadly there is no way to conditionally include a syntax construct like + * a new operator in a way that allows older versions of PHP to still + * parse the file. Instead, we will try a loop unrolling technique that + * works for 0-10 arguments. If we are passed 11 or more arguments we will + * take the performance penalty of using + * `ReflectionClass::newInstanceArgs()` to construct the desired object. + * + * @param string $clazz Class name + * @param array $args Constructor arguments + * @return mixed Constructed instance + */ + public static function constructClassInstance( $clazz, $args ) { + // TODO: when PHP min version supported is >=5.6.0 replace this + // function body with `return new $clazz( ... $args );`. + $obj = null; + switch ( count( $args ) ) { + case 0: + $obj = new $clazz(); + break; + case 1: + $obj = new $clazz( $args[0] ); + break; + case 2: + $obj = new $clazz( $args[0], $args[1] ); + break; + case 3: + $obj = new $clazz( $args[0], $args[1], $args[2] ); + break; + case 4: + $obj = new $clazz( $args[0], $args[1], $args[2], $args[3] ); + break; + case 5: + $obj = new $clazz( + $args[0], $args[1], $args[2], $args[3], $args[4] + ); + break; + case 6: + $obj = new $clazz( + $args[0], $args[1], $args[2], $args[3], $args[4], + $args[5] + ); + break; + case 7: + $obj = new $clazz( + $args[0], $args[1], $args[2], $args[3], $args[4], + $args[5], $args[6] + ); + break; + case 8: + $obj = new $clazz( + $args[0], $args[1], $args[2], $args[3], $args[4], + $args[5], $args[6], $args[7] + ); + break; + case 9: + $obj = new $clazz( + $args[0], $args[1], $args[2], $args[3], $args[4], + $args[5], $args[6], $args[7], $args[8] + ); + break; + case 10: + $obj = new $clazz( + $args[0], $args[1], $args[2], $args[3], $args[4], + $args[5], $args[6], $args[7], $args[8], $args[9] + ); + break; + default: + // Fall back to using ReflectionClass and curse the developer + // who decided that 11+ args was a reasonable method + // signature. + $ref = new ReflectionClass( $clazz ); + $obj = $ref->newInstanceArgs( $args ); + } + return $obj; + } } diff --git a/tests/phpunit/includes/libs/ObjectFactoryTest.php b/tests/phpunit/includes/libs/ObjectFactoryTest.php index 577dc3c763..622fce2fdf 100644 --- a/tests/phpunit/includes/libs/ObjectFactoryTest.php +++ b/tests/phpunit/includes/libs/ObjectFactoryTest.php @@ -79,6 +79,35 @@ class ObjectFactoryTest extends PHPUnit_Framework_TestCase { $this->assertInternalType( 'string', $obj->setterArgs[0] ); $this->assertSame( 'unwrapped', $obj->setterArgs[0] ); } + + /** + * @covers ObjectFactory::constructClassInstance + * @dataProvider provideConstructClassInstance + */ + public function testConstructClassInstance( $args ) { + $obj = ObjectFactory::constructClassInstance( + 'ObjectFactoryTestFixture', $args + ); + $this->assertSame( $args, $obj->args ); + } + + public function provideConstructClassInstance() { + // These args go to 11. I thought about making 10 one louder, but 11! + return array( + '0 args' => array( array() ), + '1 args' => array( array( 1, ) ), + '2 args' => array( array( 1, 2, ) ), + '3 args' => array( array( 1, 2, 3, ) ), + '4 args' => array( array( 1, 2, 3, 4, ) ), + '5 args' => array( array( 1, 2, 3, 4, 5, ) ), + '6 args' => array( array( 1, 2, 3, 4, 5, 6, ) ), + '7 args' => array( array( 1, 2, 3, 4, 5, 6, 7, ) ), + '8 args' => array( array( 1, 2, 3, 4, 5, 6, 7, 8, ) ), + '9 args' => array( array( 1, 2, 3, 4, 5, 6, 7, 8, 9, ) ), + '10 args' => array( array( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ) ), + '11 args' => array( array( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ) ), + ); + } } class ObjectFactoryTestFixture { -- 2.20.1