ツイート
シェア
LINEで送る
B! はてぶでブックマーク
Pocketでブックマーク
RSSフィード

PHP, Closure::call(), 一時的なバインドで無名関数の実行。'()'とはちがう。

php
イラストダウンロードサイト【イラストAC】
の画像をもとに加工しています。

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公式ドキュメント

型演算子 - instanceof

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公式ドキュメント

クラスメソッド - Closure - call

前の投稿
PHP, Closureクラス, 無名関数(クロージャ)が返すオブジェクトの型
PHP, Closure::fromCallable(), 関数名の文字列から関数を実行する。
次の投稿

コメントを残す