#はじめに
以前は、サーバーでスケジュールしたいタスクごとにcronの設定を記述していたかもしれません。しかし、これではタスクスケジュールがソース管理から外れてしまい、既存のcron設定を確認したり追加したりするためにサーバーへSSH接続しなければならず、管理が煩雑になります。
Laravelのコマンドスケジューラーは、サーバー上のスケジュールタスク管理に新しいアプローチを提供します。スケジューラーを使うと、Laravelアプリケーション内でコマンドスケジュールを流暢かつ表現豊かに定義できます。スケジューラーを利用する場合、サーバー上には単一のcronエントリだけで済みます。タスクスケジュールはapp/Console/Kernel.phpファイルのscheduleメソッド内で定義します。開始しやすいように、メソッド内にシンプルな例が用意されています。
#スケジュールの定義
アプリケーションのApp\Console\Kernelクラスのscheduleメソッドで、すべてのスケジュールタスクを定義できます。まずは例を見てみましょう。この例では、毎日深夜に呼び出されるクロージャをスケジュールし、その中でデータベースのテーブルをクリアするクエリを実行します:
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Facades\DB;
class Kernel extends ConsoleKernel
{
/**
* アプリケーションのコマンドスケジュールを定義します。
*/
protected function schedule(Schedule $schedule): void
{
$schedule->call(function () {
DB::table('recent_users')->delete();
})->daily();
}
}
クロージャを使ったスケジューリングに加え、invokableオブジェクトもスケジュールできます。invokableオブジェクトは__invokeメソッドを持つシンプルなPHPクラスです:
$schedule->call(new DeleteRecentUsers)->daily();
スケジュールされたタスクの概要や次回実行予定時刻を確認したい場合は、schedule:list Artisanコマンドを使えます:
php artisan schedule:list
#Artisanコマンドのスケジューリング
クロージャのスケジューリングに加え、Artisanコマンドやシステムコマンドもスケジュールできます。例えば、commandメソッドを使って、コマンド名またはクラス名でArtisanコマンドをスケジュールできます。
Artisanコマンドをクラス名でスケジューリングする場合、コマンド実行時に渡す追加のコマンドライン引数を配列で指定できます:
use App\Console\Commands\SendEmailsCommand;
$schedule->command('emails:send Taylor --force')->daily();
$schedule->command(SendEmailsCommand::class, ['Taylor', '--force'])->daily();
#キュージョブのスケジューリング
jobメソッドを使ってキュージョブをスケジュールできます。このメソッドは、ジョブをキューに投入するためにクロージャを定義するcallメソッドを使わずに済む便利な方法です:
use App\Jobs\Heartbeat;
$schedule->job(new Heartbeat)->everyFiveMinutes();
jobメソッドにはオプションで第2引数と第3引数を渡せます。これらはジョブを投入するキュー名とキュー接続名を指定します:
use App\Jobs\Heartbeat;
// "sqs"接続の"heartbeats"キューにジョブを投入します...
$schedule->job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();
#シェルコマンドのスケジューリング
execメソッドを使ってOSコマンドを実行できます:
$schedule->exec('node /home/forge/script.js')->daily();
#スケジュール頻度のオプション
すでにいくつかの例で、指定した間隔でタスクを実行する方法を見てきました。しかし、タスクに割り当てられるスケジュール頻度はもっと多彩です:
| メソッド | 説明 |
|---|---|
->cron('* * * * *'); |
カスタムcronスケジュールでタスクを実行 |
->everySecond(); |
毎秒タスクを実行 |
->everyTwoSeconds(); |
2秒ごとにタスクを実行 |
->everyFiveSeconds(); |
5秒ごとにタスクを実行 |
->everyTenSeconds(); |
10秒ごとにタスクを実行 |
->everyFifteenSeconds(); |
15秒ごとにタスクを実行 |
->everyTwentySeconds(); |
20秒ごとにタスクを実行 |
->everyThirtySeconds(); |
30秒ごとにタスクを実行 |
->everyMinute(); |
毎分タスクを実行 |
->everyTwoMinutes(); |
2分ごとにタスクを実行 |
->everyThreeMinutes(); |
3分ごとにタスクを実行 |
->everyFourMinutes(); |
4分ごとにタスクを実行 |
->everyFiveMinutes(); |
5分ごとにタスクを実行 |
->everyTenMinutes(); |
10分ごとにタスクを実行 |
->everyFifteenMinutes(); |
15分ごとにタスクを実行 |
->everyThirtyMinutes(); |
30分ごとにタスクを実行 |
->hourly(); |
毎時タスクを実行 |
->hourlyAt(17); |
毎時17分にタスクを実行 |
->everyOddHour($minutes = 0); |
奇数時間ごとにタスクを実行 |
->everyTwoHours($minutes = 0); |
2時間ごとにタスクを実行 |
->everyThreeHours($minutes = 0); |
3時間ごとにタスクを実行 |
->everyFourHours($minutes = 0); |
4時間ごとにタスクを実行 |
->everySixHours($minutes = 0); |
6時間ごとにタスクを実行 |
->daily(); |
毎日深夜にタスクを実行 |
->dailyAt('13:00'); |
毎日13:00にタスクを実行 |
->twiceDaily(1, 13); |
毎日1:00と13:00にタスクを実行 |
->twiceDailyAt(1, 13, 15); |
毎日1:15と13:15にタスクを実行 |
->weekly(); |
毎週日曜0:00にタスクを実行 |
->weeklyOn(1, '8:00'); |
毎週月曜8:00にタスクを実行 |
->monthly(); |
毎月1日0:00にタスクを実行 |
->monthlyOn(4, '15:00'); |
毎月4日15:00にタスクを実行 |
->twiceMonthly(1, 16, '13:00'); |
毎月1日と16日13:00にタスクを実行 |
->lastDayOfMonth('15:00'); |
毎月最終日15:00にタスクを実行 |
->quarterly(); |
毎四半期の初日0:00にタスクを実行 |
->quarterlyOn(4, '14:00'); |
毎四半期4日14:00にタスクを実行 |
->yearly(); |
毎年1月1日0:00にタスクを実行 |
->yearlyOn(6, 1, '17:00'); |
毎年6月1日17:00にタスクを実行 |
->timezone('America/New_York'); |
タスクのタイムゾーンを設定 |
これらのメソッドは追加の制約と組み合わせて、特定の曜日だけに実行されるような細かいスケジュールも作成できます。例えば、毎週月曜日にコマンドを実行するようスケジュールできます:
// 毎週月曜13時に1回実行...
$schedule->call(function () {
// ...
})->weekly()->mondays()->at('13:00');
// 平日の8時から17時まで毎時実行...
$schedule->command('foo')
->weekdays()
->hourly()
->timezone('America/Chicago')
->between('8:00', '17:00');
追加のスケジュール制約の一覧は以下の通りです:
| メソッド | 説明 |
|---|---|
->weekdays(); |
平日のみタスクを実行 |
->weekends(); |
週末のみタスクを実行 |
->sundays(); |
日曜日のみタスクを実行 |
->mondays(); |
月曜日のみタスクを実行 |
->tuesdays(); |
火曜日のみタスクを実行 |
->wednesdays(); |
水曜日のみタスクを実行 |
->thursdays(); |
木曜日のみタスクを実行 |
->fridays(); |
金曜日のみタスクを実行 |
->saturdays(); |
土曜日のみタスクを実行 |
->days(array|mixed); |
指定した曜日のみタスクを実行 |
->between($startTime, $endTime); |
指定した時間帯のみタスクを実行 |
->unlessBetween($startTime, $endTime); |
指定した時間帯はタスクを実行しない |
->when(Closure); |
真偽テストに基づいてタスクを制限 |
->environments($env); |
指定した環境でのみタスクを実行 |
#曜日制約
daysメソッドを使うと、タスクの実行を特定の曜日に限定できます。例えば、日曜日と水曜日に毎時実行するようスケジュールできます:
$schedule->command('emails:send')
->hourly()
->days([0, 3]);
または、タスクを実行する曜日を定義する際に、Illuminate\Console\Scheduling\Scheduleクラスの定数を使うこともできます:
use Illuminate\Console\Scheduling\Schedule;
$schedule->command('emails:send')
->hourly()
->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);
#時間帯制約
betweenメソッドを使うと、タスクの実行を1日の特定の時間帯に限定できます:
$schedule->command('emails:send')
->hourly()
->between('7:00', '22:00');
同様に、unlessBetweenメソッドは指定した時間帯の間はタスクを実行しないようにできます:
$schedule->command('emails:send')
->hourly()
->unlessBetween('23:00', '4:00');
#真偽テスト制約
whenメソッドは、与えられた真偽テストの結果に基づいてタスクの実行を制限できます。つまり、指定したクロージャがtrueを返す場合に限り、他の制約がなければタスクが実行されます:
$schedule->command('emails:send')->daily()->when(function () {
return true;
});
skipメソッドはwhenの逆の役割を果たします。skipがtrueを返すと、スケジュールされたタスクは実行されません:
$schedule->command('emails:send')->daily()->skip(function () {
return true;
});
複数のwhenメソッドを連結した場合、すべてのwhen条件がtrueを返したときのみスケジュールされたコマンドが実行されます。
#環境制約
environmentsメソッドを使うと、指定した環境(APP_ENVの環境変数で定義)でのみタスクを実行できます:
$schedule->command('emails:send')
->daily()
->environments(['staging', 'production']);
#タイムゾーン
timezoneメソッドを使うと、スケジュールタスクの時間を指定したタイムゾーンで解釈させることができます:
$schedule->command('report:generate')
->timezone('America/New_York')
->at('2:00')
すべてのスケジュールタスクに同じタイムゾーンを繰り返し設定する場合は、App\Console\KernelクラスにscheduleTimezoneメソッドを定義すると便利です。このメソッドは、すべてのスケジュールタスクに割り当てるデフォルトのタイムゾーンを返します:
use DateTimeZone;
/**
* スケジュールイベントでデフォルトで使用するタイムゾーンを取得します。
*/
protected function scheduleTimezone(): DateTimeZone|string|null
{
return 'America/Chicago';
}
一部のタイムゾーンはサマータイムを利用しています。サマータイムの変更時には、スケジュールタスクが2回実行されたり、まったく実行されなかったりすることがあります。このため、可能な限りタイムゾーンを使ったスケジューリングは避けることを推奨します。
#タスクの重複防止
デフォルトでは、前回のタスクがまだ実行中でもスケジュールされたタスクは実行されます。これを防ぐには、withoutOverlappingメソッドを使います:
$schedule->command('emails:send')->withoutOverlapping();
この例では、emails:send のArtisanコマンドが、すでに実行中でなければ毎分実行されます。withoutOverlapping メソッドは、実行時間が大きく変動するタスクに特に有効で、タスクの所要時間を正確に予測できない場合の重複実行を防ぎます。
必要に応じて、「重複しない」ロックが解除されるまでの分数を指定できます。デフォルトでは、ロックは24時間後に解除されます:
$schedule->command('emails:send')->withoutOverlapping(10);
withoutOverlapping メソッドは内部でアプリケーションのキャッシュを使ってロックを取得します。必要に応じて、schedule:clear-cache Artisanコマンドでこれらのキャッシュロックをクリアできます。通常は、予期しないサーバーの問題でタスクが停止した場合のみ必要です。
#1台のサーバーでタスクを実行する
この機能を利用するには、アプリケーションのデフォルトキャッシュドライバーとして database、memcached、dynamodb、または redis を使用している必要があります。また、すべてのサーバーが同じ中央キャッシュサーバーと通信している必要があります。
スケジューラーが複数のサーバーで動作している場合、スケジュールされたジョブを特定の1台のサーバーだけで実行するよう制限できます。例えば、毎週金曜の夜に新しいレポートを生成するタスクがあるとします。スケジューラーが3台のワーカーサーバーで動作していると、そのタスクは3台すべてで実行され、レポートが3回生成されてしまいます。これは望ましくありません。
タスクを1台のサーバーだけで実行したい場合は、スケジュール定義時に onOneServer メソッドを使います。最初にタスクを取得したサーバーがジョブに対してアトミックロックを確保し、他のサーバーが同時に同じタスクを実行するのを防ぎます:
$schedule->command('report:generate')
->fridays()
->at('17:00')
->onOneServer();
#1台サーバージョブの名前付け
同じジョブを異なるパラメータでスケジュールしつつ、それぞれのジョブの組み合わせを1台のサーバーで実行したい場合があります。その場合は、name メソッドで各スケジュール定義に一意の名前を付けます:
$schedule->job(new CheckUptime('https://laravel.com'))
->name('check_uptime:laravel.com')
->everyFiveMinutes()
->onOneServer();
$schedule->job(new CheckUptime('https://vapor.laravel.com'))
->name('check_uptime:vapor.laravel.com')
->everyFiveMinutes()
->onOneServer();
同様に、スケジュールされたクロージャも1台のサーバーで実行する場合は名前を付ける必要があります:
$schedule->call(fn () => User::resetApiRequestCount())
->name('reset-api-request-count')
->daily()
->onOneServer();
#バックグラウンドタスク
デフォルトでは、同時刻にスケジュールされた複数のタスクは、schedule メソッド内で定義された順に順次実行されます。長時間実行されるタスクがあると、後続のタスクの開始が大幅に遅れることがあります。すべてのタスクを同時に実行したい場合は、runInBackground メソッドを使ってバックグラウンド実行できます:
$schedule->command('analytics:report')
->daily()
->runInBackground();
runInBackground メソッドは、command と exec メソッドでスケジュールされたタスクでのみ使用できます。
#メンテナンスモード
アプリケーションがメンテナンスモードの場合、スケジュールされたタスクは実行されません。これは、メンテナンス中のサーバーに影響を与えないためです。ただし、メンテナンスモード中でもタスクを強制的に実行したい場合は、タスク定義時に evenInMaintenanceMode メソッドを呼び出します:
$schedule->command('emails:send')->evenInMaintenanceMode();
#スケジューラーの実行
スケジュールされたタスクの定義方法を学んだので、次は実際にサーバーで実行する方法を説明します。schedule:run Artisanコマンドは、すべてのスケジュールタスクを評価し、サーバーの現在時刻に基づいて実行が必要か判断します。
Laravelのスケジューラーを使う場合、サーバーに schedule:run コマンドを毎分実行するcron設定を1つだけ追加すれば十分です。cron設定の追加方法がわからない場合は、Laravel Forgeのようなサービスを利用するとcron設定を管理してくれます。
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
#1分未満のスケジュールタスク
ほとんどのOSでは、cronジョブは最大1分に1回しか実行できません。しかし、Laravelのスケジューラーは1秒ごとなど、より短い間隔でタスクをスケジュールできます:
$schedule->call(function () {
DB::table('recent_users')->delete();
})->everySecond();
1分未満のタスクが定義されている場合、schedule:run コマンドはすぐに終了せず、現在の1分間の終了まで実行を続けます。これにより、必要なすべての1分未満タスクをその間に呼び出せます。
1分未満のタスクが予想より長くかかると、後続のタスクの実行が遅れる可能性があるため、すべての1分未満タスクはキュージョブやバックグラウンドコマンドをディスパッチして処理することを推奨します:
use App\Jobs\DeleteRecentUsers;
$schedule->job(new DeleteRecentUsers)->everyTenSeconds();
$schedule->command('users:delete')->everyTenSeconds()->runInBackground();
#1分未満タスクの中断
schedule:run コマンドは、分未満の間隔で実行されるタスクが定義されている場合、実行が開始された分の間(1分間)動作します。そのため、アプリケーションをデプロイする際にはこのコマンドを中断する必要があることがあります。そうしないと、既に実行中の schedule:run インスタンスは現在の分が終わるまで、以前にデプロイされたコードを使い続けます。
進行中の schedule:run を中断するには、デプロイスクリプトに schedule:interrupt コマンドを追加します。このコマンドはデプロイ完了後に実行してください:
php artisan schedule:interrupt
#ローカルでスケジューラーを実行する
通常、ローカル開発環境にスケジューラーのcron設定は追加しません。その代わりに、schedule:work Artisanコマンドを使います。このコマンドはフォアグラウンドで動作し、終了するまで毎分スケジューラーを呼び出します:
php artisan schedule:work
#タスクの出力
Laravelスケジューラーは、スケジュールタスクの出力を扱う便利なメソッドを提供します。まず、sendOutputTo メソッドで出力をファイルに送信し、後で確認できます:
$schedule->command('emails:send')
->daily()
->sendOutputTo($filePath);
出力をファイルに追記したい場合は、appendOutputTo メソッドを使います:
$schedule->command('emails:send')
->daily()
->appendOutputTo($filePath);
emailOutputTo メソッドを使うと、出力を指定したメールアドレスに送信できます。メール送信前にLaravelのメールサービスを設定してください:
$schedule->command('report:generate')
->daily()
->sendOutputTo($filePath)
->emailOutputTo('[email protected]');
スケジュールされたArtisanまたはシステムコマンドが非ゼロ終了コードで終了した場合のみ出力をメール送信したい場合は、emailOutputOnFailure メソッドを使います:
$schedule->command('report:generate')
->daily()
->emailOutputOnFailure('[email protected]');
emailOutputTo、emailOutputOnFailure、sendOutputTo、および appendOutputTo メソッドは、command と exec メソッドでスケジュールされたタスクでのみ使用できます。
#タスクフック
before と after メソッドを使うと、スケジュールタスクの実行前後にコードを指定できます:
$schedule->command('emails:send')
->daily()
->before(function () {
// タスクが実行される直前...
})
->after(function () {
// タスクが実行された後...
});
onSuccess と onFailure メソッドは、スケジュールタスクが成功または失敗した場合に実行するコードを指定できます。失敗は、Artisanまたはシステムコマンドが非ゼロ終了コードで終了したことを意味します:
$schedule->command('emails:send')
->daily()
->onSuccess(function () {
// タスクが成功した...
})
->onFailure(function () {
// タスクが失敗した...
});
コマンドの出力が利用可能な場合、after、onSuccess、onFailure フックのクロージャで Illuminate\Support\Stringable インスタンスを $output 引数として型指定することでアクセスできます:
use Illuminate\Support\Stringable;
$schedule->command('emails:send')
->daily()
->onSuccess(function (Stringable $output) {
// タスクが成功した...
})
->onFailure(function (Stringable $output) {
// タスクが失敗した...
});
#URLのピング
pingBefore と thenPing メソッドを使用すると、スケジューラはタスクの実行前または実行後に指定したURLへ自動的に ping を送信できます。このメソッドは、スケジュールされたタスクが開始したことや実行を終了したことを Envoyer のような外部サービスに通知するのに便利です:
$schedule->command('emails:send')
->daily()
->pingBefore($url)
->thenPing($url);
pingBeforeIf と thenPingIf メソッドは、条件が true の場合のみ指定したURLをピングします:
$schedule->command('emails:send')
->daily()
->pingBeforeIf($condition, $url)
->thenPingIf($condition, $url);
pingOnSuccess と pingOnFailure メソッドは、タスクが成功または失敗した場合にのみ指定したURLをピングします。失敗は、Artisanまたはシステムコマンドが非ゼロ終了コードで終了したことを意味します:
$schedule->command('emails:send')
->daily()
->pingOnSuccess($successUrl)
->pingOnFailure($failureUrl);
すべてのピングメソッドは Guzzle HTTP ライブラリを必要とします。Guzzleは通常、新しいLaravelプロジェクトにデフォルトでインストールされていますが、誤って削除された場合はComposerで手動インストールできます:
composer require guzzlehttp/guzzle
#イベント
必要に応じて、スケジューラーが発行するイベントをリッスンできます。通常、イベントリスナーのマッピングはアプリケーションの App\Providers\EventServiceProvider クラスで定義します:
/**
* アプリケーションのイベントリスナーマッピング。
*
* @var array
*/
protected $listen = [
'Illuminate\Console\Events\ScheduledTaskStarting' => [
'App\Listeners\LogScheduledTaskStarting',
],
'Illuminate\Console\Events\ScheduledTaskFinished' => [
'App\Listeners\LogScheduledTaskFinished',
],
'Illuminate\Console\Events\ScheduledBackgroundTaskFinished' => [
'App\Listeners\LogScheduledBackgroundTaskFinished',
],
'Illuminate\Console\Events\ScheduledTaskSkipped' => [
'App\Listeners\LogScheduledTaskSkipped',
],
'Illuminate\Console\Events\ScheduledTaskFailed' => [
'App\Listeners\LogScheduledTaskFailed',
],
];