37
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PHPにクロージャがあるのに、__invokeは必要なのか?

Last updated at Posted at 2017-02-27

結論から言いますと、__invokeにしかできないことがあります。それはクロージャにクラスの機能を持たせるということです。

クロージャとinvokeの書き方の違いをサンプルで確認

まずクロージャ(無名関数)とマジックメソッド__invokeのコードを見てみましょう。どちらも関数を引数として渡すために使っています。

<?php
// difference_closure_invoke.php
function run(callable $before_func)
{
    $before_func();
    echo 'run' . PHP_EOL;

}

// クロージャ(無名関数)
$before = function () {
    echo 'anonymous before' . PHP_EOL;
};
run($before);
// => anonymous before
//    run

// invokeのおかげでインスタンスを関数呼出しできます。
// その時に実行されるメソッドが__invokeです。
class Before
{
    public function __invoke()
    {
        echo 'class before' . PHP_EOL;
    }
}
run(new Before);
// => class before
//    run

クロージャとinvokeの使い分けの基準

今回はどちらもやっていることは同じです。関数を渡すだけならクロージャの方が手軽だと思います。関数を引数として渡すなら下記の使い分けが出来ると思います。

  • その場で書き捨てのコールバック関数なら、クロージャを使う。
  • 他でも再利用するコールバック関数なら、クラス(__invoke)を使う。

クロージャを再利用しようとすると、変数にバインドしないといけません。

<?php
$before = function () {
    echo 'anonymous before' . PHP_EOL;
};

このファイルをrequireした時に、既に$beforeという変数がある場合は上書きしまいますし、$beforeがグローバル変数になってしまいます。

しかし、クラスで用意しておけば、run(new Before);のように変数にバインドする必要もありません。バインドするとしてもその変数のスコープはローカルに留めることが出来ます。

これで関数が他のクラスと同じ扱いで、再利用が可能になりました。

invokeしかできないこと

__invokeを実装しているのはクラスです。クラスということはプロパティやメソッドを持つことができます。__invokeの処理が長い時にはprivateメソッドで分割することもできますし、インスタンスを関数呼出しせずに通常のオブジェクトとして使うこともできます。

クロージャにクラスとしての機能をつけたい時に__invokeを使うといいでしょう。関数呼び出しが可能なオブジェクトを、引数として渡した後に通常のオブジェクトとして利用している例です。

<?php
// invoke_as_normal_object.php

class Before
{
    private $request;

    public function __invoke($request)
    {
        $this->request = $request;
    }

    public function getRequest()
    {
        return $this->request;
    }
}

function run($request, callable $before_func)
{
    $before_func($request);
    // ビジネスロジック...
    $response = new StdClass;
    return $response;
}

$request = new StdClass;
$request->from = 'japan';
$before = new Before();

$response = run($request, $before);
echo $before->getRequest()->from;
// => japan

runの中の最初の時点での$requestrunが終わった後に、$beforeを通じて取り出しています。これだけなら、runに渡す$requestを使えばよく特に意味は無いです。
ですが、このようにコールバックに渡ってきたオブジェクトや、コールバック内の結果を、関数が終わった後にコールバックオブジェクトを通じて取得したりできます。これを利用してできそうなことを考えてみます。

コンストラクタの代わりにコールバックで値をセットする

下記はHTTPクライアントを使って、取得したページ内容から必要な値だけをオブジェクトにセットする例です。架空のクラスを使用しています。

<?php
class PageItem
{
  private $title; // タイトルタグ
  private $lead; // 記事の最初の140文字

  public function __invoke($response)
  {
      // HTTPレスポンスの中身をセレクタによって取得する
      $this->title = $response->find('title');
      $this->title = $response->find('.article > .body > .lead');
  }
  public function getTitle()
  {
      return $title;
  }
  public function getLead()
  {
      return $lead;
  }
}
$page_item = new PageItem();

// httpリクエストを送り、レスポンスを取得することができるクラス
$client = new HttpClient();
$request_uri = 'http://example.com';
$client->get($request_uri, $page_item);

echo 'title: ' . $page_item->getTitle() . PHP_EOL;
echo 'lead: ' . $page_item->lead() . PHP_EOL;

この場合だと、ページを取得した時点ではないと必要な値が分からないため、コンストラクタで値のセットはできません。そこで__invokeを使ってみました。

ミドルウェアはクラスで作るといいのでは?

より好ましいミドルウェアにより、ルートフィルターは非推奨となりました。

cite: アップグレードガイド 5.1 Laravel

Laravelは5.1からミドルウェアが推奨されました。Slim 3でもミドルウェアがあります。

※ 私はどちらのフレームワークも使ったことはありません。

ミドルウェアとはアクションメソッドをラッピングするクロージャのことです。クロージャの中でアクションメソッドを好きな位置で呼び出せるようになるので、その前後に好きな処理を書くことが出来ます。
また、ミドルウェアをミドルウェアでラッピングするすることができるので、ログイン処理のレイヤー、IPアドレスで弾くレイヤーなど、アクションメソッドまでの処理をレイヤーとして分けて書くことができます。

ミドルウェアでクロージャが使えるということは、__invokeを実装したクラスも使えますね。再利用しないとしても、スコープが小さく留めたまま好きに書けるので、保守もしやすいですし書きやすいです。そのためレイヤーごとにクラスを用意していくのに__invokeは使えるのではないかなと思いました。

37
36
7

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
37
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?