PHP でのユニットテスト用に protected や private を一撃で引っ剥がすやつを作った

もう `Reflection` なんて要らない。 `reveal()` の一撃で終わらせましょう。

```php
<?php

use function Wazly\Revelation\reveal;

$obj = new class {
    private function do() {
        return 'プライベートメソッドにアクセスできちゃった！';
    }
};

echo reveal($obj)->do(); // "プライベートメソッドにアクセスできちゃった！"
```

https://github.com/KNJ/revelation

## ユニットテストのつらみ

PHPUnit を使ってユニットテストを書いていてつらいのが public でないメソッドを単体でテストするときです。テストを実行するオブジェクトのクラススコープとテスト対象オブジェクトのクラススコープが異なるため、 protected あるいは private で定義されたメソッドやプロパティにアクセスできないのです。それを解決する方法は、サンプルコードとともにネット上にたくさん転がっているので、本稿では特に説明しません。

## 汎用パッケージ化

よく紹介されている `Reflection` や `Closure` を用いたサンプルコードは醜いです。しかし、それ以外に方法はありません。ただ、もっと簡単かつ汎用的に扱えるモノが欲しかったので冒頭例のように超簡単に non-public を擬似的に public にするツールを作りました。

## 使い方

ここからは使い方を紹介します。

### サポートする PHP バージョン

- 7.1 以上

### インストール

Composer を使ってください。テストでしか使わないと思うので `require --dev` で。

```sh
composer require --dev knj/revelation
```

### Revelation オブジェクトを作る

private や protected をアクセス可能にしたいオブジェクトを Revelation オブジェクト (以下、 Revelation) によってラップします。実にこれだけです。例えば次のような Stuff クラスがあるとしましょう。

```php
<?php

class Stuff
{
    private $privateProperty;

    // $privateProperty に $a + $b をセット
    public function __construct($a, $b)
    {
        $this->privateProperty = $a + $b;
    }

    // $privateProperty に $x + $y をセット
    // 自分自身を返す 
    private function privateMethod($x, $y)
    {
        $this->privateProperty = $x + $y;

        return $this;
    }
}
```

次のように `reveal()` を使って、 Stuff オブジェクトの Revelation を作ります。

```php

<?php

use function Wazly\Revelation\reveal;

$stuff = new Stuff(1, 2);
$stuff = reveal($stuff);
```

これで private なメソッドやプロパティにアクセスできます。

```php

echo $stuff->privateProperty;  // 3
$stuff->privateMethod(1, 100);
echo $stuff->privateProperty;  // 101
```

Revelation の作り方は他にもいくつかあります。

```php
$stuff = new Stuff(1, 2);

// use function Wazly\Revelation\reveal;
reveal($stuff);
reveal(Stuff::class, 1, 2);
reveal(function ($a, $b) { return new Stuff($a, $b); }, 1, 2);

// use Wazly\Revelation;
Revelation::wrap($stuff);
Revelation::wrap(Stuff::class, 1, 2);
Revelation::wrap(function ($a, $b) { return new Stuff($a, $b); }, 1, 2);
```

### Revelation オブジェクトから素 (もと) のオブジェクトを取り出す

`$stuff` は Revelation という全く別のオブジェクトに置き換わっています。 Revelation が参照している素のオブジェクトを取り出したいときは `getOriginal` メソッドを使います。

```php
$stuff = reveal($stuff);
echo get_class($stuff);                // Wazly\Revelation
echo get_class($stuff->getOriginal()); // Stuff
```

### 素のオブジェクトに対する参照

同じオブジェクトから作られた Revelation は同じそのオブジェクトに対する参照を持っています。次の例では、 `$rev1` と `$rev2` は別々の Revelation オブジェクトですが、参照している元のオブジェクト ( `$stuff` ) は同じなので、1つの `$stuff` を共有している状態になっています。

```php
$rev1 = reveal($stuff);
$rev1->privateMethod(1, 2);

$rev2 = reveal($stuff);
$rev2->privateMethod(3, 4);

echo $rev1->privateProperty; // 7 not 3
echo $rev2->privateProperty; // 7
```

このようなふるまいを回避したい場合には、 `Revelation::clone` を使います[^1] 。

```php
$rev1 = reveal($stuff);
$rev1->privateMethod(1, 2);

$rev2 = Revelation::clone($stuff);
$rev2->privateMethod(3, 4);

echo $rev1->privateProperty; // 3
echo $rev2->privateProperty; // 7
```

### メソッドチェーン

Revelation ラップされたオブジェクトのメソッドが `return $this` をするとき、この `$this` はラップしている Revelation に上書きされます。これによって public でないメソッドを連鎖させることができます。

```php
reveal($stuff)->privateMethod(1, 2)->privateMethod(3, 4);
```

### static メソッドとプロパティ

public でない static メソッドやプロパティは (public なそれらと異なり) インスタンス化していないとアクセスできないので、インスタンスを Revelation ラップしてから `Revelation::getStatic` や `Revelation::callStatic` を使ってアクセスします。

```php
class A
{
    protected static $staticProperty = 'static';

    protected static function className()
    {
        return __CLASS__;
    }

    protected static function selfName()
    {
        return self::className();
    }

    protected static function staticName()
    {
        return static::className();
    }
}

class B extends A
{
    protected static function className()
    {
        return __CLASS__;
    }
}

echo reveal(B::class)->getStatic('staticProperty'); // static
echo reveal(B::class)->callStatic('className');     // B
echo reveal(B::class)->callStatic('selfName');      // A
echo reveal(B::class)->callStatic('staticName');    // B
```

## どういう仕組み？

擬似的な public 化には `Closure::bindTo` しか使ってません。`Closure::bindTo` とはなんぞや？という人は[公式リファレンス](http://php.net/manual/ja/closure.bindto.php)へGO！簡単に説明すると、クロージャ内の `$this` に任意のオブジェクトを指定し、実行時のクラススコープも変更できてしまうものです。これを使ってメソッドを実行するクロージャを作り、スコープを合わせてしまえば private でも呼び出せてしまうということです。

Revelation によるラップの仕組みは [PHPでメソッドの挙動を柔軟に変更するプロキシ設計](https://qiita.com/KNJ/items/084e4edd7954e43a6c01) を参照してください。

## 追記

Twitter で空リプをいただいたような気がするので。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">クロージャに <a href="https://twitter.com/search?q=%24this&amp;src=ctag&amp;ref_src=twsrc%5Etfw">$this</a> をバインドして可変長呼び出しをすれば万能かと言ったらそんなこともなく、変数リファレンスとマジックメソッドは併用できないんですよね。 <a href="https://t.co/Oe5QMmT1tv">https://t.co/Oe5QMmT1tv</a></p>&mdash; にゃんだーすわん (@tadsan) <a href="https://twitter.com/tadsan/status/1074978287846273029?ref_src=twsrc%5Etfw">2018年12月18日</a></blockquote>

確かにエラーになってしまいます。

```
Fatal error: Method Wazly\Revelation::__call() cannot take arguments by reference in ...
```

ケースとしてはそこまで多くないと思うので、専用のメソッドを用意して対応しようかと思います。ありがとうございました。

## 追記2

バインドされたオブジェクトを持つクロージャを返すAPI `Revelation::bind` を追加しました。これによって、少し遠回りで冗長になりますが、リファレンスを渡すメソッドも扱えるようになります。

```php

class X
{
    private function callPassByReferenceMethod($val, &$ref)
    {
        static::passByReference($val, $ref);
    }

    private static function passByReference($val, &$ref)
    {
        $ref = $ref ?? 1;
        $ref += $val;
    }
}

$closure1 = reveal(X::class)->bind(function ($val, &$ref) {
    $this->callPassByReferenceMethod($val, $ref);
});

$closure2 = reveal(X::class)->bind(function ($val, &$ref) {
    static::passByReference($val, $ref);
});

$closure1(99, $ref);
echo $ref; // 100
$closure2(100, $ref);
echo $ref; // 200
```

## 参考

- [private,protectedなメソッドを呼び出してテストする\(新\) \- Qiita](https://qiita.com/tadsan/items/8e178d6d3d19202590df)

[^1]: 内部で `clone` を使っているので `__clone()` がコールされます。