プログラミングを普通にしていたらprivate
やprotected
なプロパティやメソッドにアクセスすることはないかもしれません。
しかし、ユニットテストでprivateなプロパティの値を設定しておいてテストを実行する、みたいなことをしたくなることがあるかもしれません。また、プライベートなメソッドもテストを書きたいかもしれません。
そんなときにどのように書くのでしょうか。Reflectionを使うという方法もあるかと思います。この記事では別の方法を紹介したいと思います。
プライベートメソッドはテストを書くべきか
プライベートメソッドのテストを書くべきか、という議論はあると思うので、今回の記事ではプライベートメソッドのテストも書きたいということにしておきましょう。書くべきなのかという話についてはt-wadaさんの以下の記事が参考になるかなと思います。
自分もPHPでPHPUnitを使ってテストを書いていたのですが、privateなプロパティを事前に操作したり、テストを実行した結果プロパティがどう変わっているかを確認したり、プライベートなメソッドもテストしたくなったりしました。
そんなときにReflectionを使わずに別の方法でやりました、という紹介になります。
Closure::bindとは
Closure::bind
というのを利用しました。
ドキュメントにはこのように書かれています。
バインドされたオブジェクトとクラスのスコープでクロージャを複製する
public static Closure::bind(Closure $closure, ?object $newThis, object|string|null $newScope = "static"): ?Closure
引数は以下の3つです。
パラメータ | 説明 |
---|---|
Closure $closure | バインドする無名関数。 |
?object $newThis | 指定した無名関数をバインドするオブジェクト。クロージャのバインドを解除するには null を指定します。 |
object | string|null $newScope|クロージャを関連づけるクラススコープ、あるいは 'static' で現在のスコープを維持します。 オブジェクトを渡した場合は、そのオブジェクトの型をその代わりに使います。 これは、バインドしたオブジェクトの protected メソッドや private メソッドのアクセス権を決めます。 このパラメータに、内部クラスのオブジェクトを渡すことはできません。 |
戻り値には、新しい Closure
オブジェクトを返します。 失敗した場合は null
を返します。
使い方としては、バインドしたいオブジェクトを第2引数で渡しつつ、第3引数でそのオブジェクトのスコープを渡す。そのオブジェクトに対して実行したい無名関数を第1引数に指定する感じでしょうか。
実際にプログラムを書いてアクセスしてみる
サンプルとして、以下のような犬クラスを作成してみました。
<?php class Dog { public function __construct( private string $name, ) { } private function getName(): string { return $this->name; } private function setName($name): void { $this->name = $name; } } $pochi = new Dog('ポチ');
単純に__construct()で名前を渡して、getName()で取得するだけです。
また、PHP8.0から使えるようになったコンストラクタのプロモーションを利用しています。
PHP 8.0.0 以降では、コンストラクタの引数を 対応するオブジェクトのプロパティに昇格させることができます。 コンストラクタの引数をプロパティに代入し、それ以外の操作を行わないことはよくあることです。 コンストラクタのプロモーションは、こういった場合の短縮記法を提供します。
しかし、このクラスの良くないところとして、getName()
やsetName()
がprivateに設定されているので、外から呼ぶことができません。こんな実装をする人はいないとは思うのですが、サンプルなのでね。
privateなプロパティにアクセスする
まずは、privateで宣言されているクラスプロパティの$name
にアクセスしてみたいなと思います。
<?php $name = Closure::bind( fn () => $this->name, $pochi, $pochi::class )(); echo $name . "\n";
実行結果
ポチ
このように、Closure::bind
を利用してprivateで宣言されている$nameの値を取得することができました。
もちろん、値の変更も可能です。
Closure::bind( fn ($name) => $this->name = $name, $pochi, $pochi::class )('チョコ'); $name = Closure::bind( fn () => $this->name, $pochi, $pochi::class )(); echo $name . "\n";
実行結果
チョコ
このように、引数で渡しつつ、値を上書くことができます。
privateなメソッドを呼ぶ
次にprivateで宣言されたgetName()
を呼んでみたいと思います。
$name = Closure::bind( fn () => $this->getName(), $pochi, $pochi::class )(); echo $name . "\n";
実行結果
チョコ
このように、getName()を呼び、戻り値を受け取ることができました。
また、メソッドの引数に値を渡すこともできます。
Closure::bind( fn ($name) => $this->setName($name), $pochi, $pochi::class )('シロ'); $name = Closure::bind( fn () => $this->getName(), $pochi, $pochi::class )(); echo $name . "\n";
実行結果
シロ
さいごに
このようにClosure::bind
を利用することで、プライベートなメソッドやプロパティにアクセスすることができました。Reflectionを使うよりシンプルにかけるので個人的には好きかもしれないです。