読者です 読者をやめる 読者になる 読者になる

uopz 拡張で DI っぽいことしてみる

PHP

opcache のオプションを確認するために PHP のマニュアルを見ていたら uopz という面白そうな拡張を見つけたので試してみました。

インストール

Linux な人は pecl でインストールできます。

$ 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

サンプルのソースコード

作成したコードは下記においていおます。