opcache のオプションを確認するために PHP のマニュアルを見ていたら uopz という面白そうな拡張を見つけたので試してみました。
インストール
$ pecl install uopz
Windows な人はビルド済バイナリがあるので、自分の環境の PHP に合ったものをダウンロードしてください。
インストールが済んだら php.ini などで拡張モジュールをロードします。
zend_extension = uopz.so uopz.overloads = 1
README.md によると、opcache よりも先にロードする必要があるようです。また、記載はありませんが、xdebug よりも後にロードしないと後述のオーバーロードが動作しませんでした。
php -v
で下記のような順番で表示されれば大丈夫だと思います。
PHP 5.5.14 (cli) (built: Jun 25 2014 12:40:48) Copyright (c) 1997-2014 The PHP Group Zend Engine v2.5.0, Copyright (c) 1998-2014 Zend Technologies with Xdebug v2.2.5, Copyright (c) 2002-2014, by Derick Rethans with uopz v2.0.5, Copyright (c) 2014, by Joe Watkins <krakjoe@php.net> with Zend OPcache v7.0.4-dev, Copyright (c) 1999-2014, by Zend Technologies
ざっくりと
概ね runkit と同じようなことが出来るようですが・・・
クラスの親を実行時に変更したり、
<?php class A {} class B {} class C extends A {} var_dump(class_parents(C::class)); // array(1) { // 'A' => // string(1) "A" // } uopz_extend(C::class, B::class); var_dump(class_parents(C::class)); // array(1) { // 'B' => // string(1) "B" // }
トレイトを実行時に適用したり、
<?php class A {} trait T { public function hoge() {} } var_dump(class_uses(A::class)); // array(0) { // } uopz_extend(A::class, T::class); var_dump(class_uses(A::class)); // array(1) { // 'T' => // string(1) "T" // }
インタフェースを実行時にインプリメントしたり、
<?php class A {} interface I {} var_dump(class_implements(A::class)); // array(0) { // } uopz_implement(A::class, I::class); var_dump(class_implements(A::class)); // array(1) { // 'I' => // string(1) "I" // }
runkit でもできないことが手軽にできます。
opcode のオーバーロード
uopz_overload()
関数を使うと、一部の opcode をオーバーロードできます。
どういうことかと言うと・・・
README.md のサンプルそのままですが、exit を無視したり、
<?php uopz_overload(ZEND_EXIT, function(){}); exit(); // この exit は無視される echo "I will be displayed\n"; uopz_overload(ZEND_EXIT, null); exit(); // この exit は有効なので以降のコードは実行されない echo "I will not be displayed\n";
new をオーバーロードして new したものと異なるクラスをインスタンス化したり出来ます。
<?php class A {} class B {} var_dump(get_class(new A)); // string(1) "A" uopz_overload(ZEND_NEW, function (&$class) { if ($class === 'A') { $class = 'B'; } }); var_dump(get_class(new A)); // string(1) "B"
new のオーバーロードで DI っぽいことをする
new のオーバーロードを使えば DI っぽいことが new で実現できそうな気がしたのでやってみました。
.
.
.
次のようなクラスがあったとします。
<?php interface SayInterface { public function say(); } class Ore implements SayInterface { public function say() { return "oreore"; } } class Are implements SayInterface { private $name; /** * @var SayInterface */ private $say; public function __construct($name) { $this->name = $name; } public function setSay(SayInterface $say) { $this->say = $say; } public function say() { return "$this->name and {$this->say->say()}"; } } class Sore implements SayInterface { private $name; public function __construct($name) { $this->name = $name; } public function say() { return $this->name; } }
普通に Ore クラスを new すると、あたりまえですが Ore クラスのインスタンスが得られます。
<?php echo (new Ore())->say() . PHP_EOL; // oreore
次ように Di を定義すると、
<?php Di::register([ 'Ore' => [ 'class' => 'Are', 'params' => ['are'], 'setters' => [ 'setSay' => [Di::lazyNew('Sore')], ], ], 'Sore' => [ 'class' => 'Sore', 'params' => ['sore'], ], ]);
Ore クラスを new しようとしたときに、実際には Are クラスがインスタンス化され、コンストラクタ引数に "are" を注入し、setSay メソッドで Sore を注入します。
なお、Sore は Sore クラスのインスタンスで、コンストラクタ引数で "sore" を注入します。
<?php echo (new Ore())->say() . PHP_EOL; // are and sore
空配列で再定義すると定義は消えるので Ore クラスがインスタンス化されるように戻ります。
<?php Di::register([]); echo (new Ore())->say() . PHP_EOL; // oreore
別の設定で再定義すれば、その通りにインスタンス化されます。
<?php Di::register([ 'Ore' => [ 'class' => 'Sore', 'params' => ['dare?'], ], ]); echo (new Ore())->say() . PHP_EOL; // dare?
なお、実際にはインスタンス化するクラスを継承したクラスを実行時に定義し、そのクラスをインスタンス化しています。
そのため get_class
などでクラス名を取得すると変なクラス名になります。
<?php echo get_class(new Ore()) . PHP_EOL; // uopz.Ore.Sore
サンプルのソースコード
作成したコードは下記においていおます。