Closureクラスは無名関数(クロージャ)、アロー関数の型として使用します。
そのオブジェクトは変数に代入して 『$test()』のように変数に '()' を付けて関数を実行しますが、もうひとつ、Closure::call()を使っても実行可能。
同じ無名関数の実行なんですが、根本的なちがいがある。
バインドするクラスオブジェクト指定とクロージャの実行を同時に行う
Closure::call() の特長は、バインドするクラスオブジェクトを指定するのと同時に、クロージャの実行まですることです。
class Test
{
private string $val = '';
public function __construct(string $val)
{
$this->val = $val;
}
public function get_val()
{
return $this->val;
}
}
$test1 = new Test('test1');
$test2 = new Test('test2');
// クロージャの定義
$closure = fn($addition) => $this->val = $this->val . '-' . $addition;
var_dump($test1->get_val());
var_dump($test2->get_val());
// クロージャの実行
var_dump($closure->call($test1, 'aaa'));
var_dump($closure->call($test2, 'bbb'));
var_dump($test1->get_val());
var_dump($test2->get_val());
string(5) "test1"
string(5) "test2"
string(9) "test1-aaa"
string(9) "test2-bbb"
string(9) "test1-aaa"
string(9) "test2-bbb"
Closureインスタンスには、バインドという機能があり、クロージャとクラスオブジェクト(インスタンス)をつなげることを言います。
クロージャ($closure)は本来、クラスオブジェクトとは何の関係もないので、$this->val は意味不明のはずです。
しかし、call()メソッドでクラスオブジェクトを指定することで、このクロージャ内の$thisはそのクラスオブジェクトを指します。
だから処理が正常に行われるんですね?
ちなみに、このクロージャではアロー関数を使ってます。アロー関数ってなんだ? と思う人はこちらをどうぞ。
バインドのリセットは不可
Closure::bind() や Closure::bindTo() は、クラスオブジェクトのnull指定でバインドのリセットができるんですが、call()メソッドはできません。
さっきのコードを使ってみましょう。
$closure = fn($addition) => $this->val = $this->val . '-' . $addition;
$closure->call(null, 'aaa');
PHP Fatal error: Uncaught TypeError: Closure::call(): Argument #1 ($newThis) must be of type object, null given in /home/vagrant/php-test.php:2
Stack trace:
#0 /home/vagrant/php-test.php(4): Closure->call()
#1 {main}
thrown in /home/vagrant/php-test.php on line 2
バインドのリセットとは、つなぐクラスオブジェクトを未指定にすることです。
call()メソッドは、それがコールされるまでバインドされてない状態(リセット状態)なので、たとえ処理ができたとしても、$thisは未定義変数になります。
ノンバインドでクロージャを実行するにはシンプルに '()' でできます。
これで $this が使えないことを確認しましょう。
$closure = fn($addition) => $this->val = $this->val . '-' . $addition;
$closure('aaa');
PHP Fatal error: Uncaught Error: Using $this when not in object context in /home/vagrant/php-test.php:2
Stack trace:
#0 /home/vagrant/php-test.php(2): {closure}()
#1 {main}
thrown in /home/vagrant/php-test.php on line 2
クロージャ内で クラスオブジェクト参照をしなければ処理は正常に動きます。
$closure = fn($addition) => 'sample-' . $addition;
$result = $closure('aaa');
var_dump($result);
string(10) "sample-aaa"
() でのクロージャ実行はノンバインドでも可。
call()メソッドはバインド必須。
同じことを bind(), bindTo()メソッドを使ってやってみる
call()メソッドでしたことは、bind(), bindTo()メソッドを使って同じことができます。一気にサンプルコードを実行してみましょう。
class Test
{
private string $val = '';
public function __construct(string $val)
{
$this->val = $val;
}
public function get_val()
{
return $this->val;
}
}
$test = new Test('test1');
$closure = fn($addition) => $this->val = $this->val . '-' . $addition;
var_dump($test->get_val());
// call()
var_dump($closure->call($test, 'call()'));
var_dump($test->get_val());
// bindTo()
$new_closure1 = $closure->bindTo($test, 'Test');
var_dump($new_closure1('bindTo()'));
var_dump($test->get_val());
// bind()
$new_closure2 = Closure::bind($closure, $test, 'Test');
var_dump($new_closure2('bind()'));
var_dump($test->get_val());
string(5) "test1"
string(12) "test1-call()"
string(12) "test1-call()"
string(21) "test1-call()-bindTo()"
string(21) "test1-call()-bindTo()"
string(28) "test1-call()-bindTo()-bind()"
string(28) "test1-call()-bindTo()-bind()"
call()は『bind() or bindTo() + ()でのクロージャ実行』をギュッとまとめたもの。
まとめて処理ができるのでよく使う方法です。
call() = bind() or bindTo() + ()でのクロージャの実行
また、このクロージャでは、クラスのprivateプロパティの参照・変更、privateメソッドの実行も可能です。
それについてはこちらをどうぞ。
あと、call()はクロージャの実行結果を返すので、バインドしたクロージャの使い回しはできません。
使い回すなら、バインドしたクロージャのオブジェクトを返す、bind(), bindTo() のほうがコードとしてスマートです。
クラスオブジェクトを使わずクロージャひとつでできるから。
var_dump($test->get_val());
// call()
var_dump($closure->call($test, 'call():1'));
var_dump($closure->call($test, ':2'));
var_dump($closure->call($test, ':3'));
var_dump($test->get_val());
// 使い回すなら下記のほうがいい。
// bindTo()
$new_closure1 = $closure->bindTo($test, 'Test');
var_dump($new_closure1('bindTo():1'));
var_dump($new_closure1(':2'));
var_dump($new_closure1(':3'));
var_dump($test->get_val());
// bind()
$new_closure2 = Closure::bind($closure, $test, 'Test');
var_dump($new_closure2('bind():1'));
var_dump($new_closure2(':2'));
var_dump($new_closure2(':3'));
var_dump($test->get_val());
string(5) "test1"
string(14) "test1-call():1"
string(17) "test1-call():1-:2"
string(20) "test1-call():1-:2-:3"
string(20) "test1-call():1-:2-:3"
string(31) "test1-call():1-:2-:3-bindTo():1"
string(34) "test1-call():1-:2-:3-bindTo():1-:2"
string(37) "test1-call():1-:2-:3-bindTo():1-:2-:3"
string(37) "test1-call():1-:2-:3-bindTo():1-:2-:3"
string(46) "test1-call():1-:2-:3-bindTo():1-:2-:3-bind():1"
string(49) "test1-call():1-:2-:3-bindTo():1-:2-:3-bind():1-:2"
string(52) "test1-call():1-:2-:3-bindTo():1-:2-:3-bind():1-:2-:3"
string(52) "test1-call():1-:2-:3-bindTo():1-:2-:3-bind():1-:2-:3"
staticな処理はできない
call()メソッドの最初のパラメータはクラスのオブジェクト(インスタンス)です。
したがってクロージャ内でクラス内でstaticなプロパティ、メソッドに関する処理はできません。
一方、bind(), bindTo()メソッドでは可能。
それについてはすでに書いたのでこちらをどうぞ。
クロージャはそのままに使うクラスの変更も可能
call()メソッドは、バインドするクラスの型がそれぞれちがっても、同じクロージャで別型のクラスオブジェクトをバインドして変更できます。
ただしクロージャ内の処理は、どのクラスの型でもエラーにならないように共通処理にする必要がある。
instanceof演算子を使って処理を分けてもいい。
PHP公式ドキュメント
interface TestInterface
{
public function get();
}
class Message implements TestInterface
{
private string $msg = '';
public function __construct(string $msg)
{
$this->msg = $msg;
}
public function get()
{
return $this->msg;
}
}
class Price implements TestInterface
{
private int $price = 0;
public function __construct(int $price)
{
$this->price = $price;
}
public function get()
{
return $this->price;
}
}
class Car implements TestInterface
{
private string $maker = '';
public function __construct(string $maker)
{
$this->maker = $maker;
}
public function get()
{
return $this->maker;
}
}
$message = new Message('OK?');
$price = new Price(12000);
$car = new Car('TOYOTA');
$closure = fn() => $this->get();
var_dump($closure->call($message));
var_dump($closure->call($price));
var_dump($closure->call($car));
string(3) "OK?"
int(12000)
string(6) "TOYOTA"
これはcall()メソッド独自の仕様ではなく、bind(), bindTo()でも同じです。
Closureクラスのバインド機能の仕様。
PHP公式ドキュメント