【Laravel】Task Scheduling(スケジューラ)で定期処理を実装する

laravelアイキャッチ Laravel

こんにちは、かつコーチです。

前回はQueueを使って、時間のかかる処理を非同期化する方法を解説しました。

今回は、その延長線上にある「定期処理」の実装方法です。

「毎日深夜に古いデータを削除したい」「毎朝レポートをメールで送りたい」——こういった「決まった時間に自動で処理を動かしたい」場面は、実務でかなり頻繁に出てきます。

LaravelにはTask Scheduling(タスクスケジューラ)という仕組みが標準で用意されているので、今回はその使い方を基礎から見ていきます。

Task Schedulingとは?

cronを直接書くのとの違い

定期処理といえば、Linuxのcron(決まった時刻にコマンドを実行するOSの仕組み)を思い浮かべる人も多いと思います。

昔ながらのやり方だと、サーバーのcrontabに直接コマンドを何行も書いていました。

0 0 * * * php /var/www/artisan emails:send
0 3 * * * php /var/www/artisan cleanup:old-logs
30 6 * * 1 php /var/www/artisan report:weekly

これでも動きますが、コマンドが増えるたびにサーバーに入ってcrontabを編集する必要があり、変更履歴もGitで追えません。

Laravelのタスクスケジューラは、この「いつ・何を実行するか」という定義をPHPのコードとしてアプリケーション内に書けるようにしたものです。

サーバー側の設定は1行だけでいい

Laravelのスケジューラを使う場合、サーバー側のcrontabに登録するのは、実質1行だけです。

* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

これは「1分ごとに schedule:run を実行する」という意味です。

schedule:run が呼ばれるたびに、Laravel側で「今この瞬間に実行すべきタスクがあるか」をチェックし、該当するものだけを実行してくれます。

つまり、実際に「何を・いつ実行するか」というスケジュール定義はすべてアプリケーションのコード側に集約できる、という発想です。

私はこの仕組みを最初に知ったとき、「サーバー側の設定を1行に固定できる」というメリットの大きさにすぐには気づけませんでした。

実際にチームで開発するようになってから、「新しい定期処理を増やすたびにインフラ担当に連絡してcrontabを書き換えてもらう」という手間がまるごとなくなったのを実感し、ありがたみが分かりました。

基本の書き方・実装手順

手順1:スケジュールの定義場所

Laravel 11以降では、routes/console.php の中に Schedule ファサードを使ってスケジュールを定義します(Laravel 10以前は app/Console/Kernel.phpschedule メソッドに書きます)。

<?php
// routes/console.php

use Illuminate\Support\Facades\Schedule;

Schedule::command('cleanup:old-logs')->daily();

Schedule::command() の引数には、Artisanコマンドの名前を文字列で指定します。

->daily() の部分が「いつ実行するか」を決める頻度指定で、これを「毎日0時に実行する」という意味に変換してくれます。

手順2:頻度の指定方法いろいろ

頻度の指定方法はdaily()以外にもたくさん用意されています。

<?php
use Illuminate\Support\Facades\Schedule;

// 毎分実行
Schedule::command('report:tick')->everyMinute();

// 毎時0分に実行
Schedule::command('cache:refresh')->hourly();

// 毎日深夜3時に実行
Schedule::command('cleanup:old-logs')->dailyAt('03:00');

// 毎週月曜の朝6時30分に実行
Schedule::command('report:weekly')->weeklyOn(1, '06:30');

// 平日の午前9時のみ実行
Schedule::command('reminder:send')->weekdays()->at('09:00');

dailyAt()weeklyOn() を使うと、時刻や曜日まで細かく指定できます。

「毎日」「毎週」だけでなく、「平日だけ」「特定の曜日だけ」といった条件も、メソッドチェーンだけで表現できるのがLaravelスケジューラの読みやすいところです。

手順3:クロージャで直接処理を書くこともできる

Artisanコマンドをわざわざ作らなくても、クロージャ(無名関数)を直接スケジュールに登録することも可能です。

<?php
use Illuminate\Support\Facades\Schedule;
use Illuminate\Support\Facades\DB;

Schedule::call(function () {
    DB::table('sessions')
        ->where('last_activity', '<', now()->subDays(30)->timestamp)
        ->delete();
})->daily();

ただし、処理が複雑になってくる場合は、後からテストしやすいようにArtisanコマンドとして切り出しておく方がメンテナンスしやすくなります。

シンプルな処理はクロージャ、ある程度まとまった処理はコマンド化、という使い分けを意識するとよいでしょう。

つまずきやすい設定・注意点:ローカルでの動作確認

スケジューラをローカル環境で試すとき、1分ごとにcronが動くのを待つのは非効率です。

そういうときは、schedule:run を手動で叩くと、その瞬間に「実行条件を満たしているタスク」だけがすぐに実行されます。

php artisan schedule:run

また、「今どんなタスクが登録されているか」を一覧で確認したいときは、次のコマンドが便利です。

php artisan schedule:list

私が実装中によくやるのは、->everyMinute() で一時的に頻度を上げて動作確認し、確認できたら本来の頻度(->daily()など)に戻す、というやり方です。

いきなり本番相当の頻度でテストすると「本当に動いているのか、まだ実行タイミングが来ていないだけなのか」が分かりにくいので、この方法をおすすめします。

よくあるつまずきポイント・エラー対処

つまずき:cron側の設定を忘れて何も動かない

Laravel側でどれだけ丁寧にスケジュールを書いても、サーバー側のcrontabに schedule:run の登録がなければ、何も実行されません。

❌ Before:スケジュール定義だけ書いてcron登録を忘れる

<?php
// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('report:daily')->dailyAt('08:00');

このコードだけをデプロイしても、サーバーの crontab -e に何も登録していなければ、schedule:run 自体がいつまでも呼ばれず、レポートは永遠に送信されません。

私も初めてスケジューラを本番に導入したとき、「コードはデプロイしたのに動かない」と1時間ほど悩みました。

原因はいたってシンプルで、サーバー側のcron登録を忘れていただけでした。

✅ After:サーバー側にcronを1行登録する

# crontab -e で以下を追加
* * * * * cd /var/www/your-project && php artisan schedule:run >> /dev/null 2>&1

デプロイ後は必ず crontab -l でcronの登録内容を確認する、という手順をチェックリストに加えておくと安心です。

Laravel Forgeなど一部のホスティングサービスでは、このcron登録が自動で設定される場合もありますが、自前のサーバーで運用する場合は必ず自分の目で確認しましょう。

つまずき:タスクが重複して実行される

サーバーを複数台にスケールさせている場合、それぞれのサーバーで schedule:run が独立して動いてしまい、同じタスクが同時に複数実行されてしまうことがあります。

これを防ぐには、onOneServer() を使います。

<?php
use Illuminate\Support\Facades\Schedule;

Schedule::command('report:daily')
    ->dailyAt('08:00')
    ->onOneServer();

onOneServer() を付けておくと、複数台のサーバーで同時にスケジュールが動くタイミングでも、Laravel側が排他制御をしてくれて、実際に処理を実行するのは1台だけになります。

複数サーバー構成を組む予定がなくても、将来スケールする可能性がある処理には最初から付けておくと安心です。

応用・一歩先の使い方

実行結果を通知する

タスクが失敗したときにSlackやメールで通知を受け取りたい場合は、onFailure() を組み合わせます。

<?php
use Illuminate\Support\Facades\Schedule;

Schedule::command('report:daily')
    ->dailyAt('08:00')
    ->onOneServer()
    ->onFailure(function () {
        \Illuminate\Support\Facades\Log::error('日次レポートの送信に失敗しました');
    });

本番運用では、定期処理が「静かに失敗し続けている」ことに気づけないケースが一番怖いです。

onFailure() で失敗を検知できるようにしておくと、エラーが起きたときにすぐ気づけます。

Queueと組み合わせて重い処理を逃がす

前回解説したQueueと組み合わせると、スケジューラから重い処理をキューに逃がすこともできます。

<?php
use Illuminate\Support\Facades\Schedule;
use App\Jobs\GenerateMonthlyReport;

Schedule::job(new GenerateMonthlyReport())->monthlyOn(1, '01:00');

Schedule::job() を使えば、指定した頻度で自動的にジョブをキューへ投入できます。

「定期的に起動したいが、処理自体は重いのでバックグラウンドで処理したい」というケースでは、この組み合わせが定番になります。

まとめ

この記事のポイント

  • Laravelのタスクスケジューラを使うと、サーバー側のcrontab設定は1行だけで済み、スケジュール定義はコードとして管理できる
  • Schedule::command()->daily()->dailyAt() などを組み合わせて頻度を指定する
  • デプロイ後はサーバー側のcron登録を忘れずに確認する(動かないときの定番の原因)
  • 複数サーバー構成では onOneServer() で重複実行を防ぐ
  • Schedule::job() を使えばQueueと組み合わせて重い定期処理を実装できる

次に読むべき記事

次回からは実践Tips・エラー解決編に入り、Laravelでよく出るエラーとその解決法をまとめて紹介していきます。

→ 次の記事:Laravelでよく出るエラーと解決法まとめ

タイトルとURLをコピーしました