runkit と uopz について

# runkit とは

PHPの関数やクラスのメソッドの挙動を変更することを目的としたPHP拡張です。(pecl で配布されています)

- [pecl runkit](https://pecl.php.net/package/runkit)

プロダクションコードで使うことはないでしょうが、テストを行う際に、元のコードの挙動を少しだけ変更することによってテストする範囲を限定するといった使い方があります。

テストではPHPUnitの 「[テストダブル](https://phpunit.de/manual/current/ja/test-doubles.html)」 をうまく使えば、runkit を乱用する必要はないのですが、以下の様な場合には runkit の出番となります。

- PHPの組み込み関数の挙動を変えたい
- staticメソッドの挙動を変えたい
    - staticメソッドをモックするライブラリである [StatickMock](https://github.com/tototoshi/staticmock) は内部的に runkit を利用しています

runkit を使ったコードは以下のような感じになります。 Example クラスにもともとあった foo というメソッドの挙動を変更しています。

runkit では、もともとあったメソッドを一旦別名にして退避しておいて、あらたなメソッドを追加するという感じになります。もとに戻すときは追加したメソッドを削除し、退避しておいたメソッドを元の名前に戻すことになります。

```php
<?php
class Example
{
    public function foo()
    {
        echo "runkit!\n";
    }
}

$obj = new Example();
$obj->foo(); // runkit!

runkit_method_rename('Example', 'foo', 'bar');
var_dump(method_exists('Example', 'foo')); // false
var_dump(method_exists('Example', 'bar')); // true

$obj = new Example();
$obj->bar(); // runkit!

runkit_method_add('Example', 'foo', '$num1, $num2', 'printf("%d\n", $num1 + $num2);', RUNKIT_ACC_PUBLIC);
var_dump(method_exists('Example', 'foo')); // true
var_dump(method_exists('Example', 'bar')); // true

$obj = new Example();
$obj->foo(12, 4); // 16

runkit_method_remove('Example', 'foo');
var_dump(method_exists('Example', 'foo')); // false
var_dump(method_exists('Example', 'bar')); // true

runkit_method_rename('Example', 'bar', 'foo');
var_dump(method_exists('Example', 'foo')); // true
var_dump(method_exists('Example', 'bar')); // false

$obj = new Example();
$obj->foo(); // runkit!
```

# runkit の PHP7 対応

runkit ですが、 pecl で配布されているものは PHP 5.6 までしか対応しておらず、PHP7 では動作しません。

PHP7で動作させるには、PHP7対応されている runkit7 というものをつかうことになります。
ただし、runkit7 は pecl では配布されていません。

- [runkit7/runkit7](https://github.com/runkit7/runkit7)

runkit7 をソースから make すれば、pecl 配布されていた runkit と同じインタフェースで利用できます。

```sh
git clone https://github.com/runkit7/runkit7.git
cd runkit7
phpize
./configure
make
make test
sudo make install
```

# uopz について

PHPマニュアルの [関数リファレンス/PHPの振る舞いの変更](http://php.net/manual/ja/refs.basic.php.php) に runkit と並んで、 uopz というPHP拡張が記載されています。

- [uopz](http://php.net/manual/ja/book.uopz.php)

uopz も runkit 同様、PHPコードの挙動を変更することを目的としたライブラリで、runkit との違いは、以下のようなものとなります。

- runkit は PHP 5.3 以上で動作するが、 uopz は PHP 5.4 以上
- runkit は PHP 5.6 までしか動作しない(runkit7は別物)が、 uopz は PHP 7 対応バージョンがリリースされている

uopz も pecl で配布されていますので、 `pecl install uopz` でインストールできますし、remi repos を利用すれば、 `yum install php-pecl-uopz` でインストールすることができます。

- [pecl uopz](http://pecl.php.net/package/uopz)

uopz は PHP5.6以下の対応をしたバージョン2系と、PHP7以降の対応をしたバージョン5系があり、用意されているメソッドが変更されています。

## バージョン2系

バージョン2系はPHP5.6まで動作します。(PHP7ではバージョン5系の拡張しか動作しません)

バージョン2系は runkit とよく似たインタフェースが準備されており、メソッド/関数を別名で退避しておいて、追加して・・・といった上記で説明したのと同じようなことができました。

- runkit_method_rename に対応するのが、 [uopz_rename](http://php.net/manual/ja/function.uopz-rename.php)
- runkit_method_add に対応するのが、 [uopz_function](http://php.net/manual/ja/function.uopz-function.php)
- runkit_method_remove に対応するのが、 [uopz_delete](http://php.net/manual/ja/function.uopz-delete.php)

一旦退避するのではなく、 [uopz_backup](http://php.net/manual/ja/function.uopz-backup.php) で関数をバックアップしておいて、uopz_function で上書きし、 [uopz_restore](http://php.net/manual/ja/function.uopz-restore.php) で元に戻すといった使い方もバージョン2ではできました。

## バージョン5系

バージョン5系はPHP7以上で動作します(PHP5.6以下ではバージョン2系の拡張しか動作しません)

バージョン5系では、上記で説明した、uopz_rename/uopz_function/uopz_delete/uopz_backup/uopz_restore といったメソッドが削除されており、全く別の関数が追加されています。

[uopz_set_return](http://php.net/manual/ja/function.uopz-set-return.php) というメソッドを使えば、元の関数/メソッドをバックアップしつつ挙動を変更でき、[uopz_unset_return](http://php.net/manual/ja/function.uopz-unset-return.php) というメソッドを使うと、元の関数/メソッドの挙動に戻すことができます。

一旦退避する必要がないので、runkitの例よりも簡単に挙動を変更できます。

```php
<?php
class Example
{
    public function foo()
    {
        echo "uopz!\n";
    }
}

$obj = new Example();
$obj->foo(); // uopz!

uopz_set_return('Example', 'foo', function ($num1, $num2) {
    printf("%d\n", $num1 + $num2);
}, 1);
var_dump(method_exists('Example', 'foo')); // true
var_dump(get_class_methods('Example')); // array(0 => 'foo')

$obj = new Example();
$obj->foo(12, 4); // 16

uopz_unset_return('Example', 'foo');
var_dump(method_exists('Example', 'foo')); // true
var_dump(get_class_methods('Example')); // array(0 => 'foo')

$obj = new Example();
$obj->foo(); // uopz!
```

関数/メソッド単体で挙動を変えていく場合は、uopz_set_return / uopz_unset_return を使いますが、クラスの挙動をまるごと変更してしまっていい場合は、 [uopz_set_mock](http://php.net/manual/ja/function.uopz-set-mock.php) / [uopz_unset_mock](http://php.net/manual/ja/function.uopz-unset-mock.php) を使えば、クラス全体をまるっと挙動変更できます。

最初に紹介した staticメソッドをモックする StaticMock ですが、PHP7 で利用するためには runkit7 を使用する形になっているのですが、uopz を使うようにしてみたものが以下のものになります。

- https://github.com/kunit/staticmock/pull/1

# まとめ

runkit/uopz は便利ですが、使わないですむなら使わないほうがいいものだと思っているので、どうしても使わないといけない場合のみ使うようにしましょう。