イントロダクション
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);
});
}]);