PHP 執行期動態產生 class instance

今天在聽 小鐵 講 DI 實作 1 時提到了這個例子,當下在想如果換成 PHP 5.6 的 arguments unpack 的語法不曉得會不會動,驗證完後覺得很有趣,順便來寫一篇。

不像 Java,PHP 在執行期時才決定要產生哪個類別的 instance 或呼叫哪個函式,並不需要大費周章的用 Reflection API,但還是有些是不透過 Reflection 辦不到的事,其中產生 instance 時如果要傳入參數的話,就需要透過 ReflectionClass 來達成這個目的。

前面提到的問題,你沒辦法用 call_user_funccall_user_func_array 來達成目的,因為沒辦法滿足第一個 callable 參數...

  1. 建構子不是 static method,你沒辦法用 Foo::__construct 這種方式,如果可以的話,參數的傳遞是另外一個問題。

  2. 在執行建構子時你需要傳入 instance,你都要靠它產生 instance 了,它還跟你討 instance,這兩個條件互斥了,變成雞生蛋與蛋生雞的問題,所以沒辦法用 array($instance, '__construct')

PoC 的程式碼如下,跑一下 benchmark

<?php
// bench.php
function bench($callback)
{
    $start = microtime(true);
    $callback();
    return (microtime(true) - $start);
}

class Foo
{
    public function __construct($a, $b, $c)
    {
        // var_dump(func_get_args());
    }
}

$class_name = 'Foo';
$loop = 5000000;

$elapsed_time = bench(
    function() use ($class_name, $loop) {
        global $argv;

        $i = 0;
        while ($i++ < $loop) {
            $ref_class = new ReflectionClass($class_name);
            $instance = $ref_class->newInstanceArgs($argv);
        }
    }
);
echo "Reflection take $elapsed_time seconds\n";

$elapsed_time = bench(
    function() use ($class_name, $loop) {
        global $argv;

        $i = 0;
        while ($i++ < $loop) {
            $instance = new $class_name(...$argv);
        }
    }
);
echo "Arguments unpack take $elapsed_time seconds\n";

輸出如下,可以看出效率快一點點,另外的優點是拿 splat operator ... 來換兩行 ReflectionClass 的寫法變的簡潔有力!

shell> time ~/.phpbrew/php/php-5.6.0RC4/bin/php bench.php foo bar
Reflection take 8.2250480651855 seconds
Arguments unpack take 2.4252769947052 seconds

real    0m10.669s
user    0m10.656s
sys     0m0.008s

提外話:

在過程中還發生另外的小插曲,在利用 phpbrew build 5.6.0RC4 來測試上面的 PoC,發現在 build unstable 的版本時動不了,動手修正後送 pull request 2 回去,不久後收到 comment 的回覆的信件時,順便被告知 PHP 5.6.0 GA 了!!也太巧了吧...XD