はじめに
このエントリーについて
この記事は「Laravelでウェブアプリケーションをつくるときのベストプラクティスを探る」シリーズの一編です。
他の記事は目次からアクセスしてください。
今回は php artisan {command}
という形で実行できるカスタムコマンドを追加するときのベストプラクティスです。
環境
- PHP 5.6
- Laravel 5.3
公式リファレンス
Console Commands - Laravel - The PHP Framework For Web Artisans
詳細
よくあるシナリオ
- コンソールから手動実行されるコマンド
- cron から自動実行されるコマンド
スケジューラーについては今回はパスしますが、2 のケースは特別な事情がない限りスケジューラーを使うのが Laravel 流みたいです。
共通のガイドライン
- コマンド名の命名規則は {app:名詞:動詞} にしましょう
- Command クラスの仕事は極力減らしましょう
- 実際の処理を行うオブジェクトは DI しましょう
手動実行されるコマンドのガイドライン
人間がコマンドを打つので、help で表示されるコメントは充実させておきましょう
- 引数には説明をつけましょう
- 成功の場合も失敗の場合も標準出力にメッセージを出しましょう
自動実行されるコマンドのガイドライン
- 失敗の場合にログにメッセージを出力しましょう
シナリオ 1 のサンプルコード
1ユーザーに対してメールを送るコマンドです。
アプリケーション固有のコマンドには app と先頭につけておくと、どんなコマンドがあるか php artisan list app
で一覧表示できるので便利です。
引数があるときは、 $signature
に {パラメータ名 : 説明} のようにコロンで区切ってプレースホルダーを書くと引数についての説明を付記できます。
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use App\Mailer;
use App\User;
class SendEmail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:email:send {user : User ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send an email to a user';
/**
* @var App\Mailer
*/
protected $mailer;
/**
* Create a new command instance.
*
* @param App\Mailer $mailer
* @return void
*/
public function __construct(Mailer $mailer)
{
parent::__construct();
$this->mailer = $mailer;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$userId = $this->argument('user');
try {
$user = User::findOrFail($userId);
$this->mailer->send($user);
} catch (ModelNotFoundException $e) {
$this->error('Error: No user with ID(' . $userId . ') found - Exception: ' . $e->getMessage());
return;
} catch (Exception $e) {
$this->error('Error:' . $e->getMessage());
return;
}
$this->info('Success: Email sent to ' . $user->email);
}
}
App\Mailer
をコンストラクタインジェクションして、実際の処理はこのクラスに担当させます。基本的には、何らかのビジネスロジックはモデルやサービス側に閉じ込めて、Command クラスの責務は
- 入力パラメーターの処理
- 実行結果の処理
の2点に限定した方がいいでしょう。
ちなみに、php artisan help app:email:send
と打つとコマンドの詳細が確認できます。
» php artisan help app:email:send
Usage:
app:email:send <user>
Arguments:
user User ID
Options:
(略)
Help:
Send an email to a user
シナリオ 2 のサンプルコード
複数のユーザーに対してメールを送るコマンドで、一日に一回 cron から実行されます。
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use App\Mailer;
use App\User;
class SendEmails extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:email:send';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send an email to subscribers';
/**
* @var App\Mailer
*/
protected $mailer;
/**
* Create a new command instance.
*
* @param App\Mailer $mailer
* @return void
*/
public function __construct(Mailer $mailer)
{
parent::__construct();
$this->mailer = $mailer;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$subscribers = User::newsletterSubscribers()->get();
$nSent = 0;
foreach ($subscribers as $user) {
try {
$this->mailer->sendNewsletter($user);
++$nSent;
} catch (\Exception $e) {
$this->error('Error: failed to send to user with ID(' . $user->id . ') - Exception: ' . $e->getMessage());
}
}
$this->info('Success: Email sent to ' . $nSent . ' of ' . $subscribers->count() . ' subscribers.');
}
}
ポイントは、バッチの場合何らかのコレクションに対してループを回しますが、ループブロック内の行数は極力抑えるようにした方が、モジュラリティが高まるので、処理が長くなるようなら別クラスか別メソッドに移しましょう、ということと、途中で例外が起きても処理を続行する必要があるケースがあるので、例外は必ず捕捉して、メッセージを出しましょう、ということです (後述しますが、Command の出力は Kernel クラスにてログに出すようにしましょう)。
出力については、バッチの場合処理対象の件数が多いとログが大きくなってしまうので、成功の場合のメッセージはなくていいと思います。
ちなみに、スケジューラー (Kernel クラスの schedule メソッド) の記述は以下のようになります。
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
Commands\SendEmails::class,
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('app:email:send')
->dailyAt('20:00')->appendOutputTo(storage_path('logs/email_send.log'));
}
/**
* Register the Closure based commands for the application.
*
* @return void
*/
protected function commands()
{
require base_path('routes/console.php');
}
}
appendOutputTo
はログ保存ポリシーによっては sendOutputTo
でもいいでしょうし、emailOutputTo
と組み合せるとよりいいんじゃないでしょうか。
詳しくは公式リファレンスを。
Task Scheduling - Laravel - The PHP Framework For Web Artisans
まとめ
- コマンド名の命名規則は {app:名詞:動詞} にしましょう
- Command クラスの仕事は極力減らしましょう
- 実際の処理を行うオブジェクトは DI しましょう
- 手動実行されるコマンドは、引数には説明をつけましょう
- 手動実行されるコマンドは、成功の場合も失敗の場合も標準出力にメッセージを出しましょう
- 自動実行されるコマンドは、失敗の場合にログにメッセージを出力しましょう
他にもこんなベストプラクティスがあるよ、などコメントや編集リクエストいただけると助かります