ここで作成するプロジェクトのソースファイルは GitHub で公開しています.
Webシステムで,ユーザが何らかのボタンをクリックしたときに,メールが送信されたり,データベースにレコードが登録されたりするときの処理にある程度の時間を要することがある.このような場合,ボタンを押してから次の画面に遷移するまで時間を要することになり,ユーザ目線での使い勝手が悪くなる.このような場合の一つの解決方法はキューを用いることで,もう一つがコマンド(コンソール)を用いる方法である.
キューでは数秒から10秒程度で終了するジョブをバックグラウンドで実行するときに有用である.例えば,メールの送信をキューで実現すれば,メールの送信ボタンを押した直後に次の画面が表示されるので,ユーザはメールの送信が一瞬で終了したように感じられ使い勝手が良い.実際にはメールの送信はバックグラウンドで行われている.しかしながらキューの場合は PHP の設定にもよるが,標準では60秒でタイムアウトになってしまうため,60秒以上必要な処理は途中で強制終了してしまうという問題点がある.
一方でここで実現するコマンド(コンソール)は UNIX (Linux) の コマンドとほぼ同様に利用することができ,UNIX の nohup
コマンドおよび &
を併用すれば,バックグラウンドで処理が終了するまで実行することができるようになる.
なお,Laravel 5.7 までコンソールと呼ばれていたこの機能が,Laravel 5.8 ではコマンドと呼ばれるようになった.よって,Laravel 5.7 では php artisan make:console
であった命令が,php artisan make:command
のように変更になっていたり,その他ところどころ命令の異なる部分がある.ここでは,Laravel 5.8 をベースに作成する.
ではデータベースへのレコード登録にコマンドを用いるシステムを開発してみよう.
コメントを一気に登録する部分までは,キューの場合と全く同様であるので以下のページを参考に,コメント登録の機能まで作成しよう.(Git を使っているのであれば,該当するコミットまでリセットすると良い.この場合,データベースファイルは一度削除して再生成すると問題が起こらない.)
上の作業によって,5件の投稿をすれば7秒程度待たされる状態までできているはずである.
コマンドを生成するために,次の命令を実行する.
[vagrant@localhost laravelCommand]$ pwd ⏎ /home/vagrant/Documents/laravel/laravelCommand [vagrant@localhost laravelCommand]$ php artisan make:command StoreCommentsCommand ⏎ Console command created successfully. [vagrant@localhost laravelCommand]$
上のコマンドによって次のような app/Console/Commands/StoreCommentsCommand.php ファイルが作成された.この14行目でコマンド名を定義できることに注意しよう.
app/Console/Commands/StoreCommentsCommand.php
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class StoreCommentsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'command:name';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//
}
}
生成されたコマンドファイルを編集して,まずは5件のコメントが登録されるようなコードを記述しよう.
app/Console/Commands/StoreCommentsCommand.php
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Comment;
class StoreCommentsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'command:StoreCommentsCommand'; // ここがコマンドの名前になる
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
for ($i = 1; $i <= 5; $i++) {
usleep(1330000); // 1.33 秒スリープする
$comment = new Comment();
$comment->title = $i . '件目のコメント';
$comment->body = 'コメント' . $i . 'の本文';
$comment->save();
}
}
}
コマンドが作成できたので,php artisan
コマンドを使って実行してみよう.まずは,フォアグラウンドで実行する.この場合は,コマンドの実行を開始してからすべての処理が終わるまでおよそ7秒間待たされることになる.
[vagrant@localhost laravelCommand]$ php artisan command:StoreCommentsCommand ⏎
[vagrant@localhost laravelCommand]$ ← ← 7秒経過後に表示された
Web ブラウザで表示すると,5件のデータが追加されていることが確認できる.
次に,同じコマンドをバックグラウンドで実行してみよう.UNIX では &
を最後に入力するとバックグラウンドで処理を実行できる.また,nohup
を使えば,ユーザのログアウト後も処理を継続できる.さらに標準エラー出力を標準出力にリダイレクトするためのオプション > /dev/null
も追加する.この場合は,処理が直ちに終了したように見えるが,データベースへの登録処理がバックグラウンドで行われていることも確認しよう.
[vagrant@localhost laravelCommand]$ nohup php artisan command:StoreCommentsCommand > /dev/null & ⏎
[1] 14794
nohup: 標準入力を無視し、標準エラー出力を標準出力にリダイレクトします
[vagrant@localhost laravelCommand]$ ← ← コマンド実行開始直後に表示された
コマンドの実行開始から7秒以上経過した後にブラウザを再読み込みすると,5件がさらに登録されていることを確認できる.
上の例では,常に5件のコメントしか追加できなかった.ここでは,コマンドオプションを指定できるように修正する.
app/Console/Commands/StoreCommentsCommand.php
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Comment;
class StoreCommentsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'command:StoreCommentsCommand {cnt=5}'; // オプションを省略した場合は5件になる
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// コマンドプションを取得する
$cnt = $this->argument('cnt');
for ($i = 1; $i <= $cnt; $i++) {
usleep(1330000); // 1.33 秒スリープする
$comment = new Comment();
$comment->title = $i . '件目のコメント';
$comment->body = 'コメント' . $i . 'の本文';
$comment->save();
}
}
}
コマンドオプションの設定ができたので,今度はオプションに10を指定して,10件を一気に登録してみよう.
[vagrant@localhost laravelCommand]$ nohup php artisan command:StoreCommentsCommand 10 > /dev/null & ⏎ [1] 18585 nohup: 標準入力を無視し、標準エラー出力を標準出力にリダイレクトします [vagrant@localhost laravelCommand]$
オプションを省略した場合には,5件の登録ができることも確認しておこう.
[vagrant@localhost laravelCommand]$ nohup php artisan command:StoreCommentsCommand > /dev/null & ⏎ [2] 18632 nohup: 標準入力を無視し、標準エラー出力を標準出力にリダイレクトします [vagrant@localhost laravelCommand]$
UNIX のシェルからコマンドを実行する方法がわかった.Laravel からコマンドを実行するためには,シェルと同じコマンドを準備して PHP の exec()
関数を使えば良い.
app/Http/Controllers/CommentsController.php (抜粋)
public function store(Request $request)
{
$this->validate($request, [
'numberOfComments' => 'required|integer|min:1|max:1000'
]);
// コマンドを準備して実行する
$artisan = base_path() . '/artisan command:StoreCommentsCommand';
$argument = $request->numberOfComments;
$command = "php {$artisan} {$argument}";
// ここを有効にしてどのようなコマンドが実行されるのか確認すると良い.
// dd("nohup {$command} > /dev/null &");
exec("nohup {$command} > /dev/null &");
return redirect('/comments');
}
実際に Web ブラウザから件数を指定してボタンをクリックすると,直後に画面が遷移するが,ブラウザの再読み込みを行うことで徐々にバックグラウンドで登録されていることを確認できるはずである.また,100件(2分15秒程度)や1000件(22〜23分)といた件数であっても正しく登録できること確認しよう.