【Laravel】Artisanコマンドの自作方法|make:command・スケジューラ登録・テストまで

はじめに

定期バッチやメンテナンス処理を、コントローラやルートに直接書いてしまうと、ブラウザ経由の HTTP 経路と php artisan の CLI 経路の責務が混ざりやすくなります。

「毎日深夜に集計レポートを生成したい」「本番でだけ手動実行するメンテナンス用の CLI が欲しい」といった要件を、一時的な PHP スクリプトや cron 直書きで対応していませんか。動くこと自体は問題なくても、引数の受け渡し、ログ出力、テスト、チーム内での共有が後から難しくなることがあります。

Laravel の Artisan コマンドは、アプリケーションのサービスコンテナ(DI 機構)や設定をそのまま使える CLI エントリポイントです。クラスベースのコマンドを app/Console/Commands に置けば、Laravel 11 以降では通常は自動登録されます。さらに routes/console.php からスケジューラへ登録すれば、cron の設定はサーバーに 1 行だけ残し、実行内容はアプリケーションコード側で管理できます。

本記事では make:command での生成から、signature の設計、スケジューラ登録、コンソールテストまでを一通り整理します。挙動の細部は Laravel のメジャーにより異なる場合があるため、手元の composer.jsonlaravel/framework に合わせて確認してください。並行して Laravel 公式ドキュメントの Artisan 章Task Scheduling 章の該当バージョンを開くと再現しやすいです。

この記事では次の範囲を扱います。

  • php artisan make:command によるコマンド生成
  • signaturehandle・依存性注入の基本
  • routes/console.php へのスケジューラ登録
  • コンソールコマンドのテスト
  • 実務で起きやすい失敗例

前提と検証範囲

検証範囲は、2026-06-08 時点で確認した Laravel 12.x の Artisan 公式ドキュメントConsole Tests 章Task Scheduling 章です。

Laravel 10 以前では app/Console/Kernel.php にスケジュールを書く構成が一般的でした。Laravel 11 以降は routes/console.php が主な定義場所です。手元のバージョンに合わせて読み替えてください。

コード例は、既存の Laravel プロジェクトのルートで php artisan を実行できる状態を前提とします。

背景:クラスベースとクロージャベース

Artisan コマンドには、大きく 2 つの書き方があります。

方式置き場所向いている用途
クラスベースapp/Console/Commands引数・オプションが多い、テストしたい、DI を使う
クロージャベースroutes/console.php数行で済む簡単なコマンド

本記事の主役は、再利用とテストをしやすいクラスベースです。クロージャベースは §4 で最小例を示します。


1. make:command でコマンドを生成する

Artisan でコマンドクラスを生成します。

php artisan make:command GenerateReport

app/Console/Commands ディレクトリが無い場合は、このタイミングで作成されます。Laravel 12 では make:command--phpunit--pest を付けると、対応するテストファイルも同時に生成できます(make:command のオプション)。

生成直後のクラスには、signaturedescriptionhandle の骨格が入っています。まずは実行名(シグネチャ)と説明を埋め、処理本体は handle に書くのが基本です。

次の例では、集計処理を担う ReportGenerator サービスへ処理を委譲します。

<?php

namespace App\\Console\\Commands;

use App\\Services\\ReportGenerator;
use Illuminate\\Console\\Command;

class GenerateReport extends Command
{
    protected $signature = 'reports:generate {date?} {--dry-run}';

    protected $description = 'Generate a daily report for the given date';

    public function handle(ReportGenerator $generator): int
    {
        $date = $this->argument('date') ?? now()->toDateString();
        $dryRun = (bool) $this->option('dry-run');

        if ($dryRun) {
            $this->info("Dry run: would generate report for {$date}");
            return self::SUCCESS;
        }

        $generator->run($date);
        $this->info("Generated report for {$date}");

        return self::SUCCESS;
    }
}

ReportGenerator はアプリケーション側で用意するサービスクラスです。HTTP 経路からも同じクラスを呼べるため、バッチと Web の処理を共有しやすくなります。

<?php

namespace App\\Services;

class ReportGenerator
{
    public function run(string $date): void
    {
        // 集計・ファイル生成などの実処理
    }
}

app/Console/Commands 配下に置いたクラスは、Laravel 11 以降では通常は自動登録されます。別ディレクトリに置く場合は、bootstrap/app.phpwithCommands でスキャン対象を追加します(Registering Commands)。

登録できているかは、次で確認できます。

php artisan list
php artisan help reports:generate

2. signature で引数とオプションを定義する

signature は、ルート定義に近い 1 行でコマンド名・引数・オプションを表現するプロパティです(Defining Input Expectations)。

記法意味
{name}必須引数reports:generate {date}
{name?}任意引数reports:generate {date?}
{name=default}デフォルト付き引数(文字列リテラル)reports:generate {date=today}
{--flag}真偽オプション{--dry-run}
{--name=}値を取るオプション{--path=}

{date=today}today はデフォルト値の書き方例です。動的な日付が必要なら、§1 の例のように handle 内で now()->toDateString() を使う方が一般的です。

実行例は次のとおりです。

php artisan reports:generate 2026-06-01
php artisan reports:generate --dry-run
php artisan reports:generate 2026-06-01 --dry-run

コマンド内では $this->argument('date')$this->option('dry-run') で値を取得します。対話的な入力が必要なら $this->ask()$this->confirm() も使えますが、CI やスケジューラ実行では非対話に寄せると運用しやすいです。

3. handle にロジックを書くときのポイント

handle はコマンド実行時に呼ばれるメソッドです。戻り値を省略すると成功時は終了コード 0 になります。明示する場合は return self::SUCCESS(0)や return self::FAILURE(1)、あるいは整数を返します(Exit Codes)。

公式ドキュメントでは、コマンド本体は薄く保ち、重い処理はアプリケーションサービスへ委譲する構成が推奨されています。handle の引数に型ヒントを書くと、サービスコンテナから依存が注入されます。§1 の ReportGenerator がその例です。

出力には $this->info()$this->warn()$this->error()$this->line() が使えます。件数が多いループでは withProgressBar() で進捗表示もできます。

他の Artisan コマンドを内部から呼ぶ場合は、$this->call('migrate:status') が使えます。コントローラや別コマンドから呼ぶ場合は Artisan::call() を使います(Calling Commands From Other Commands)。

4. スケジューラに登録する

Laravel 11 以降では、スケジュール定義の主な置き場所は routes/console.php です(Task Scheduling の Introduction)。既存のクロージャコマンド定義の下に、次のように追記する形が一般的です。

<?php

use Illuminate\\Support\\Facades\\Artisan;
use Illuminate\\Support\\Facades\\Schedule;

// 既存のクロージャコマンド定義があればそのまま残す

Schedule::command('reports:generate')
    ->dailyAt('02:00')
    ->withoutOverlapping()
    ->onOneServer();

クラス名で指定する書き方もあります。

use App\\Console\\Commands\\GenerateReport;

Schedule::command(GenerateReport::class)->dailyAt('02:00');

登録内容の確認には次が使えます。

php artisan schedule:list

本番ではサーバーの cron に、公式ドキュメントどおり次の 1 行を置く運用が一般的です。

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

withoutOverlapping() は前回実行が終わるまで次の実行を抑止します。onOneServer() は複数サーバー環境での二重実行を避けるためのものです。いずれも共有キャッシュ(Redis など)が前提になるため、環境に合わせて 公式の説明を確認してください。

クロージャコマンドをスケジュールする場合

数行で済む処理なら、routes/console.php でクロージャコマンドを定義できます。

use Illuminate\\Support\\Facades\\Artisan;
use Illuminate\\Support\\Facades\\Schedule;

Artisan::command('reports:prune', function () {
    $this->info('Pruning old reports...');
})->purpose('Remove stale report files')->daily();

定義の直後に ->daily() などをチェーンしてスケジュール登録もできます(Scheduling Artisan Closure Commands)。試作には向きますが、テストや複雑な引数設計が必要になったらクラスベースへ移す判断がしやすいです。

5. コンソールコマンドをテストする

Laravel はコンソールコマンド向けのテスト API を提供しています(Console Tests)。Feature テスト(アプリ全体を起動して検証するテスト)から $this->artisan() でコマンドを起動し、終了コードや出力を検証できます。

<?php

namespace Tests\\Feature;

use Tests\\TestCase;

class GenerateReportCommandTest extends TestCase
{
    public function test_dry_run_prints_message_without_side_effects(): void
    {
        $this->artisan('reports:generate', [
                '--dry-run' => true,
            ])
            ->expectsOutputToContain('Dry run')
            ->assertSuccessful();
    }
}

DB を更新するコマンドをテストする場合は、RefreshDatabase トレイトの利用を検討してください。今回の dry-run 例では副作用がないため、トレイトは省略しています。

対話入力を伴うコマンドでは expectsQuestion()expectsConfirmation() が使えます。

$this->artisan('reports:generate')
    ->expectsConfirmation('Proceed with generation?', 'yes')
    ->assertSuccessful();

出力の厳密な一致ではなく部分一致を見たい場合は expectsOutputToContain() が便利です。デバッグ時に実際の出力文字列を取り出したい場合は、テスト内で withoutMockingConsoleOutput() を使います。実行後に Artisan::output() を参照する方法がドキュメントに載っています。

テストの実行は次のとおりです。

php artisan test --filter=GenerateReportCommandTest

実装時の注意点

シグネチャは reports:generate のように グループ:動作 形式にすると、php artisan list で探しやすくなります。既存コマンド名と衝突しないか help で確認してください。

スケジューラや監視ツールは、非ゼロ終了を失敗とみなすことがあります。想定内の失敗と異常終了を return 値で区別すると運用しやすいです。

.env の値や DB 接続に依存するコマンドは、app()->environment() や設定値で本番専用のガードを検討してください。

handle に SQL や外部 API 呼び出しをすべて直書きすると、HTTP 経路から再利用しづらくなります。§1 の ReportGenerator のように処理本体をサービスクラスへ寄せると、テストもしやすくなります。

失敗例:コマンドを作ったがスケジューラや cron を設定していない

make:command でクラスを作り、手動で php artisan reports:generate を実行したら成功した、という状態で終わることがあります。しかし routes/console.phpSchedule::command(...) を書かなければ、schedule:run はそのコマンドを起動しません。さらにサーバー側に schedule:run を毎分呼ぶ cron が無ければ、登録済みでも自動実行は起きません。

「コマンド単体は動くのに、本番で定期実行されない」という報告は、スケジュール定義と cron のどちらか(または両方)の抜けで起きやすいです。php artisan schedule:list に表示されるか、ステージングで schedule:run を手動実行して確認する手順を用意しておくと切り分けが早くなります。

学び:CLI はアプリケーションの正式な入口になる

上記の切り分けができると、「動くコマンド」と「定期実行されるコマンド」の差を早期に見つけられます。Artisan コマンドを整備すると、バッチ処理が「HTTP の裏側のスクリプト」から「アプリケーションと同じ DI・設定・ログ基盤を使う処理」へ移ります。

スケジューラとセットで設計すると、cron 設定の散在を減らせます。クラスとして diff でき、CI にテストを載せられるため、コードレビューの対象にも含めやすくなります。

手順チェックリスト

初めて自作コマンドを通すときは、次の順で確認すると抜けが少ないです。

  1. php artisan make:command でクラスを生成する
  2. php artisan listhelp で登録を確認する
  3. 手動で php artisan reports:generate --dry-run を実行する
  4. routes/console.phpSchedule::command(...) を追記する
  5. php artisan schedule:list で登録を確認する
  6. ステージングで php artisan schedule:run を実行し、出力と終了コードを見る
  7. php artisan test でコンソールテストを通す

まとめ

  • php artisan make:commandapp/Console/Commands にクラスを生成し、signaturehandle を中心に実装する。
  • Laravel 11 以降では routes/console.phpSchedule::command() で定期実行を定義する。サーバーには schedule:run 用の cron を 1 本置く。
  • $this->artisan() によるコンソールテストで、終了コードと出力を検証できる。
  • 手動実行だけ確認して終わらせると、スケジュール登録や cron 漏れに気づきにくい。

次に試せること

  1. 本記事の reports:generate をベースに、実務の集計処理を ReportGenerator へ移して handle から呼び出す。
  2. php artisan schedule:list で登録を確認し、ステージングで schedule:run を実行してターミナル出力と storage/logs/laravel.log を見る。
  3. Console Tests 章を読み、expectsConfirmation()doesntExpectOutput() を使ったテストパターンを 1 つ追加する。

Source