イントロダクション

Laravelコマンドバスは、シンプルで分かりやすい「コマンド」として実行する必要のあるアプリケーションのタスクをカプセル化する便利なメソッドです。コマンドの目的を理解するために、ポッドキャストをユーザーに購入してもらうアプリケーションを構築しているとしましょう。

ユーザーがポッドキャストを購入すると、色々な処理を実行する必要が起きます。例えば、ユーザーのクレジットカードに課金したり、領収を表すデータベースへレコードを追加したり、購入の確認メールを送ったりです。多分、そのユーザーがポッドキャストを購入できるのか、様々なバリデーションを行う必要もあるでしょう。

コントローラーメソッドへ、こうしたロジックを全部突っ込むこともできます。しかし、多くの点で良くありません。多分、コントローラは要求がされるだろう別のHTTPアクションもたくさん処理し、各コントローラーメソッドには複雑なロジックがつめ込まれ、膨れ上がり、結果として読みづらくなってしまうのが第一の欠点です。第二に、コントローラー外でポッドキャスト購入のロジックを再利用するのが難しいことです。第三に、ポッドキャスト購入ロジックをテストするため、HTTPリクエストのスタブを生成しなくてはなりませんし、アプリケーションへ送る完全なリクエストを作成しなくてはならないため、コントローラーのユニットテストが困難になることです。

ロジックをコントローラーに詰め込む代わりに、例えばPurchasePodcastコマンドのように、「コマンド」オブジェクトとしてカプセル化することも選択できます。

コマンド作成

新しいコマンドクラスは、make:command Artisan CLIで生成できます。

php artisan make:command PurchasePodcast

生成されたクラスは、app/Commandsディレクトリーへ設置されます。生成されたコマンドには、デフォルトで2つのメソッドがあります。コンストラクターとhandleメソッドです。もちろんコンストラクターはコマンドへ適切なオブジェクトを渡すために使えます。一方のhandleメソッドは、コマンドを実行します。例をご覧ください。

class PurchasePodcast extends Command implements SelfHandling {

    protected $user, $podcast;

    /**
     * 新しいコマンドインスタンス生成
     *
     * @return void
     */
    public function __construct(User $user, Podcast $podcast)
    {
        $this->user = $user;
        $this->podcast = $podcast;
    }

    /**
     * コマンド実行
     *
     * @return void
     */
    public function handle()
    {
        // ポッドキャスト購入ロジックを処理する…

        event(new PodcastWasPurchased($this->user, $this->podcast));
    }

}

handleメソッドでも依存をタイプヒントで指定でき、サービスコンテナにより自動的に注入されます。例えば:

    /**
     * コマンド実行
     *
     * @return void
     */
    public function handle(BillingGateway $billing)
    {
        // ポッドキャスト購入ロジックを処理する…
    }

コマンドデスパッチ

コマンドはできました。ではどうやってデスパッチするのでしょう?もちろん、handleメソッドを直接呼び出すこともできます。しかし、コマンドをLaravel「コマンドバス」を通して実行する方法には、後ほど説明する多くの利点があります。

アプリケーションのベースコントローラーを覗いてみれば、DispatchesCommandsトレイトが見つかります。このトレイトは、コントローラーからdispatchメソッドを呼び出せるようにします。以下のようにです。

public function purchasePodcast($podcastId)
{
    $this->dispatch(
        new PurchasePodcast(Auth::user(), Podcast::findOrFail($podcastId))
    );
}

コマンドバスはコマンド実行の面倒を見ます。そして、呼びだされたIoCコンテナがhandleメソッドへ必要な依存を注入します。

お望みであれば、どんなクラスへでもIlluminate\Foundation\Bus\DispatchesCommandsトレイトを付け加えることができます。クラスのコンストラクターを通して、コマンドバスのインスタンスを受け取りたいのであれば、Illuminate\Contracts\Bus\Dispatcherインターフェイスをタイプヒントに指定してください。最後に、Busファサードを使い、簡単にコマンドをデスパッチする方法を紹介します。

    Bus::dispatch(
        new PurchasePodcast(Auth::user(), Podcast::findOrFail($podcastId))
    );

リクエストからコマンドのプロパティーをマップする

HTTPリクエストの変数をコマンドへマップしたいと考えるのは当然でしょう。それぞれのリクエストを手動で無理やりマップする代わりに、Laravelでは簡単に実現できるヘルパーメソッドを用意しています。DispatchesCommandsトレイトで使用できる、dispatchFromメソッドを取り上げてみてみましょう。

$this->dispatchFrom('Command\Class\Name', $request);

このメソッドは指定されたコマンドクラスのコンストラクターを調べ、それからHTTPリクエスト(もしくは他のArrayAccessオブジェクト)から変数を取り出し、必要なコマンドのコンストラクター引数を埋めます。ですから、もしコマンドクラスがコンストラクターで$firstName変数を取る場合、コマンドバスはHTTPリクエストからfirstNameパラメーターを取り出そうとします。

dispatchFromメソッドはさらに第3引数に配列を指定できます。この配列はリクエストからは埋められないコンストラクター引数を埋めるために使用されます。

$this->dispatchFrom('Command\Class\Name', $request, [
    'firstName' => 'Taylor',
]);

コマンドのキュー投入

コマンドバスは現在のリクエストサイクル中で、同期的にジョブを実行するだけではなく、Laravelのキュージョブとして非同期に実行する重要な手法も提供しています。では同期的に実行する代わりに、どうやってコマンドバスへ、バックグラウンド処理を行うためにジョブをキューに入れろと指示するのでしょうか。簡単です。最初に新しいコマンドを生成するときに、--queuedフラグをコマンドへ付けるだけです。

php artisan make:command PurchasePodcast --queued

すぐ気がつくでしょうが、これによりいくつかの機能がコマンドに追加されます。すなわちIlluminate\Contracts\Queue\ShouldBeQueuedインターフェイスとSerializesModelsトレイトです。これらは、コマンドバスにコマンドをキューに投入するように指示し、同時にコマンドのプロパティーとしてストアされたEloquentモデルを優雅にシリアライズ、非シリアライズします。

もし、既に存在するコマンドをキュー実行のコマンドに変更したければ、単にIlluminate\Contracts\Queue\ShouldBeQueuedインターフェイスを手動でクラスへ実装してください。これはメソッドを含んでおらず、主にデスパッチャーがコマンドへ投入する「目印のインターフェイス」として使用しています。

それから、コマンドを通常通り書いてください。それがバスへデスパッチされると、バックグラウンドで処理するため自動的にキューへ投入します。

キューされるコマンドのインターフェイスの詳細は、キューのドキュメントを参照してください。

コマンドパイプライン

ハンドラにディスパッチされる前に、「パイプライン」中の他のクラスへ、コマンドを通すことができます。コマンドパイプラインは、実行するのがコマンドという違いだけで、まるでHTTPミドルウェアのように動作します!例えば、コマンド全体の操作をデータベーストランザクションでラップしたり、実行をログしたりできます。

パイプをバスに追加するには、App\Providers\BusServiceProvider::bootメソッドから、ディスパッチャーのpipeThroughメソッドを呼び出します。

$dispatcher->pipeThrough(['UseDatabaseTransactions', 'LogCommand']);

コマンドのパイプは、ミドルウェアと同様に、handleメソッドで定義します。

class UseDatabaseTransactions {

    public function handle($command, $next)
    {
        return DB::transaction(function() use ($command, $next)
        {
            return $next($command);
        });
    }

}

コマンドパイプのクラスは、IoCコンテナを通じて依存解決されるため、コンストラクターでタイプヒントによる依存を指定することもできます。

コマンドパイプを「クロージャー」で定義することもできます。

$dispatcher->pipeThrough([function($command, $next)
{
    return DB::transaction(function() use ($command, $next)
    {
        return $next($command);
    });
}]);