LoginSignup
99

More than 5 years have passed since last update.

PHPでクロージャのuse地獄を回避する裏ワザ

Last updated at Posted at 2016-01-23

Before

PHP7.0+
(function () {
    $name = 'John';
    (function () use ($name) {
        $greet = 'Hello';
        (function () use ($name, $greet) {
            echo "{$greet}, {$name}\n";
        })(); 
    })();
})();
PHP5.3+
call_user_func(function () {
    $name = 'John';
    call_user_func(function () use ($name) {
        $greet = 'Hello';
        call_user_func(function () use ($name, $greet) {
            echo "{$greet}, {$name}\n";
        });
    });
});

After

PHP7.0以降は new class{} で無名クラスが使えます.全てのクロージャを無名クラスに属させることで,$thisを通じた共有が実現できます.call() は第1引数で $this を指定することが出来ます.

PHP7.0+
(function () {
    $this->name = 'John';
    (function () {
        $this->greet = 'Hello';
        (function () {
            echo "{$this->greet}, {$this->name}\n";
        })();
    })();
})->call(new class{});
PHP7.0+
(function () {
    $this->name = 'John';
    (function () {
        $this->greet = 'Hello';
        (function () {
            echo "{$this->greet}, {$this->name}\n";
        })();
    })();
})->bindTo(new class{})();
PHP7.0+
\Closure::bind(function () {
    $this->name = 'John';
    (function () {
        $this->greet = 'Hello';
        (function () {
            echo "{$this->greet}, {$this->name}\n";
        })();
    })();
}, new class{})();

PHP5.4でも new \stdClass とすることで無名クラスの代わりに使えます.call()もPHP7.0以降の機能ですが,bind()__invoke()を組み合わせることで解決できます.call_user_func()を使っても構いません.

PHP5.4+
\Closure::bind(function () {
    $this->name = 'John';
    call_user_func(function () {
        $this->greet = 'Hello';
        call_user_func(function () {
            echo "{$this->greet}, {$this->name}\n";
        });
    });
}, new \stdClass)->__invoke();
PHP5.4+
call_user_func(\Closure::bind(function () {
    $this->name = 'John';
    call_user_func(function () {
        $this->greet = 'Hello';
        call_user_func(function () {
            echo "{$this->greet}, {$this->name}\n";
        });
    });
}, new \stdClass));

備考

クラスメソッド編

クラスメソッドで本来の$thisも参照しつつ…という場合も無理矢理ですが対応可能です.

プロパティを簡潔に初期化するために(object)[]という記法を使っていますが,これはstdClassにキャストされます.stdClassに対してcall()は使用出来ないため,PHP7.0以降でもPHP5.4のときと同様にbindTo()()で代用する必要があることに注意してください.

PHP7.0+
<?php

class Klass {

    private $greet = 'Hello';

    public function hello() {
        (function () {
            $this->name = 'John';
            (function () {
                (function () {
                    echo "{$this->self->greet}, {$this->name}\n";
                })();
            })();
        })->bindTo((object)['self' => $this])();
    }

}

(new Klass)->hello();
PHP5.4+
<?php

class Klass {

    private $greet = 'Hello';

    public function hello() {
        \Closure::bind(function () {
            $this->name = 'John';
            call_user_func(function () {
                call_user_func(function () {
                    echo "{$this->self->greet}, {$this->name}\n";
                });
            });
        }, (object)['self' => $this])->__invoke();
    }

}

(new Klass)->hello();

別のアプローチ

持ち回したい変数を全てあらかじめ定義したオブジェクトまたは配列に入れるようにし,最小限のuseで済ませる,というのもアリです.現実的にはこちらのほうが周囲の理解を得やすいかもしれません.

PHP7.0+ オブジェクト
$s = new class{};
(function () use ($s) {
    $s->name = 'John';
    (function () use ($s) {
        $s->greet = 'Hello';
        (function () use ($s) {
            echo "{$s->greet}, {$s->name}\n";
        })(); 
    })();
})();
PHP7.0+ 配列(配列の場合は参照で渡さないと書き換えが出来ないので注意)
$s = [];
(function () use (&$s) {
    $s['name'] = 'John';
    (function () use (&$s) {
        $s['greet'] = 'Hello';
        (function () use (&$s) {
            echo "{$s['greet']}, {$s['name']}\n";
        })(); 
    })();
})();

非同期処理ライブラリとの併用

関連投稿: 「ReactPHP?時代はRecoilPHPだ!」

この書き方が真価を発揮するのはReactPHPやRecoilPHPといった,非同期処理のためのライブラリを使う場合です.クロージャが頻繁に登場するのでuse地獄に陥りやすいです.この裏ワザを使うとかなり気持ちよく書けますね.

PHP7.0+ ReactPHP
<?php

require 'vendor/autoload.php';
$GLOBALS['loop'] = React\EventLoop\Factory::create();

function plusOneLater($a) {
    $deferred = new React\Promise\Deferred();
    $GLOBALS['loop']->addTimer(1, function () use ($deferred, $a) {
        $deferred->resolve($a + 1);
    });
    return $deferred->promise();
}

(function () {
    $this->greet = 'Hello';
    React\Promise\all([
        plusOneLater(0)->then(function ($x) { echo "$this->greet, $x\n"; return plusOneLater($x); })
                       ->then(function ($y) { echo "$this->greet, $y\n"; return plusOneLater($y); })
                       ->then(function ($z) { echo "$this->greet, $z\n"; }),
        plusOneLater(3)->then(function ($x) { echo "$this->greet, $x\n"; return plusOneLater($x); })
                       ->then(function ($y) { echo "$this->greet, $y\n"; return plusOneLater($y); })
                       ->then(function ($z) { echo "$this->greet, $z\n"; }),
    ])->then(function () { echo "All done !!\n"; });
})->call(new class{});

$GLOBALS['loop']->run();
PHP7.0+ RecoilPHP
<?php

require 'vendor/autoload.php';

function plusOneLater($a) {
    yield Recoil\Recoil::sleep(1);
    return $a + 1;
}

Recoil\Recoil::run((function () {
    $this->greet = "Hello";
    yield Recoil\Recoil::all([
        (function () {
            $x = yield plusOneLater(0); 
            echo "$this->greet, $x\n";
            $y = yield plusOneLater($x);
            echo "$this->greet, $y\n";
            $z = yield plusOneLater($y);
            echo "$this->greet, $z\n";
        })(),
        (function () {
            $x = yield plusOneLater(3);
            echo "$this->greet, $x\n";
            $y = yield plusOneLater($x);
            echo "$this->greet, $y\n";
            $z = yield plusOneLater($y);
            echo "$this->greet, $z\n";
        })(),
    ]);
    echo "All done !!\n";
})->bindTo(new class{}));

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
99