技術資料

Laravel 13でArtisanコマンドを作成する:引数・オプション・dry-runの基本

作成日:2026.09.10

Laravel 13でArtisanコマンドを作成する方法を、引数・オプション・コンソール出力・終了コードの基本から解説します。日時指定した記事を定期的に公開するDB更新処理を例に、--dry-runで対象を確認してから安全に実行する方法や、スケジューラー・cronでの定期実行についても紹介します。

Laravelで、アプリケーションの処理をCLIから実行する方法についてまとめます。

Laravelには、Artisanというコマンドラインインターフェースが用意されています。Web画面を経由せずに、バッチ処理やメンテナンス処理を実行したい場合に利用できます。

まずは、Artisanコマンドの作り方、引数とオプションの設定方法、コンソールへの出力、終了コードの扱いを確認します。その後で、日時を指定した記事を公開する処理を具体例として実装します。

カスタムコマンドの作成方法や入力値の定義については、Laravel 13.xのArtisan Console公式ドキュメントも参照してください。

Artisanコマンドを使う場面

Artisanコマンドは、ターミナルからLaravelのアプリケーションを起動して処理を実行するための入口です。例えば、次のような処理に向いています。

  • 大量のデータをまとめて処理する
  • 定期的に集計や同期を実行する
  • 管理者向けのデータ補正を行う
  • 古いデータや不要なファイルを整理する
  • デプロイ後のキャッシュ削除など、運用上の処理を行う

ブラウザから実行する処理と違い、CLIでは実行時の入力値、進捗や結果の表示、終了コードを設計できます。また、cronやLaravelのタスクスケジューラーから定期的に起動することもできます。

Artisanコマンドを作成する

カスタムArtisanコマンドは、make:commandで作成します。

php artisan make:command SampleCommand

通常は、app/Console/Commands/SampleCommand.phpが作成されます。ディレクトリがまだ存在しない場合でも、コマンドの作成時に必要なディレクトリが用意されます。

作成したコマンドは、次のように一覧やヘルプから確認できます。

php artisan list
php artisan help sample:run

一つのコマンドへ複数の目的を詰め込まず、処理の目的ごとにコマンドを分けると、実行方法とエラーの切り分けが分かりやすくなります。

Artisanコマンドの基本構造

Artisanコマンドでは、主に次の内容を定義します。

コマンド名と入力値
Signature属性または$signatureプロパティで定義します。
説明
Description属性または$descriptionプロパティで定義します。
実行処理
handleメソッドへ記述します。

Laravel 13.xでは、属性を使うと次のように記述できます。

use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;

#[Signature('sample:run {target} {--force : 確認を省略して実行する}')]
#[Description('サンプル処理を実行する')]
class SampleCommand extends Command
{
    public function handle(): int
    {
        $target = $this->argument('target');
        $force = (bool) $this->option('force');

        // $targetと$forceを使って処理する

        return self::SUCCESS;
    }
}

handleの戻り値は終了コードです。正常終了を0、失敗を表す値を0以外にすると、シェルスクリプトやスケジューラーから結果を判定しやすくなります。

プロパティ記法との違い

コマンド名と説明は、属性ではなくプロパティへ記述することもできます。

protected $signature = 'sample:run {target} {--force : 確認を省略して実行する}';

protected $description = 'サンプル処理を実行する';

属性記法は、コマンド名や説明を属性として分けて記述できます。プロパティ記法は、既存コードや過去のサンプルと合わせやすい場合があります。対象プロジェクトのLaravelバージョンと、生成されたコマンドの形式を確認して、どちらかに統一するとよいでしょう。

引数とオプションを設定する

コマンドへ実行時の値を渡したい場合は、シグネチャへ引数やオプションを記述します。

#[Signature('sample:run {target} {--category=} {--dry-run}')]

この例では、targetが引数、categoryとdry-runがオプションです。=が付いたcategoryは値を受け取り、dry-runは指定されているかどうかを確認するスイッチとして扱います。

php artisan sample:run articles --category=tech
php artisan sample:run articles --dry-run

コマンドの中では、argument()で引数、option()でオプションの値を取得します。必須入力の不足や、許可されていない値が渡された場合の扱いも、コマンド側で決めておきます。

コンソール出力と終了コードを設定する

CLIで実行する処理では、利用者が現在の状態を確認できるように、結果をコンソールへ表示します。

$this->info('処理を開始します。');
$this->line('対象: ' . $target);
$this->warn('確認が必要な状態です。');
$this->error('処理に失敗しました。');

正常終了時はself::SUCCESS、入力エラーや処理失敗時はself::FAILUREなどを返します。対象が0件だった場合を成功とみなすか、エラーとみなすかは、定期実行や運用方法に合わせて決めます。

dry-runを設計する

データを更新するコマンドでは、本実行の前に対象を確認できるdry-runを用意すると安全性を高められます。

dry-runでは、対象件数や変更予定の内容を表示します。ただし、更新処理や削除処理は実行しません。本実行とdry-runで検索条件が別々になると、確認した対象と実際に更新する対象がずれるため、対象を決める処理は共通化します。

$items = $query->get();

foreach ($items as $item) {
    $this->line($item->name);
}

if ($this->option('dry-run')) {
    $this->comment('dry-runのため、更新していません。');

    return self::SUCCESS;
}

$query->update(['status' => 'processed']);

dry-runは、更新したデータをロールバックする仕組みではありません。確認と本実行の間に別の処理が動く可能性があるため、重要な処理では排他制御や実行直前の再確認も検討します。

具体例的な実装例

ここまで確認した一般的な作り方を使い、日時を指定した記事を公開するコマンドを作成します。記事テーブルに公開日時を保存し、公開日時を過ぎた記事のステータスを更新する例です。

前提環境

今回の対象環境は以下の通りです。

  • OS: Windows 11
  • PHP: 8.3系
  • Laravel: 13.x
  • データベース: SQLiteまたはLaravelで利用できるデータベース
  • ターミナル: PowerShell

Laravelプロジェクトの作成や、ModelとMigrationの基本については、以前の記事「Windows 11でLaravel 13を始める:環境構築からルーティング・Controller・Bladeまで」も参照してください。

記事テーブルを用意する

まずは、公開日時と公開状態を保存するarticlesテーブルを用意します。既存のテーブルを使う場合は、実際のカラム名に合わせて以降のコードを読み替えてください。

ModelとMigrationを作成する

Laravelプロジェクトのルートディレクトリで、次のコマンドを実行します。

php artisan make:model Article -m

app/Models/Article.phpと、database/migrations以下のMigrationファイルが作成されます。

Migrationへ、記事のタイトル、カテゴリ、ステータス、公開日時を定義します。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('articles', function (Blueprint $table): void {
            $table->id();
            $table->string('title');
            $table->string('category');
            $table->string('status')->default('draft');
            $table->timestamp('publish_at')->nullable();
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->index(['status', 'publish_at']);
            $table->index(['status', 'category', 'publish_at']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('articles');
    }
};

statusには、今回の例ではdraft、scheduled、publishedを保存します。実際のアプリケーションでは、利用するステータスを定数やEnumで管理する方法もあります。

Articleモデルを設定する

app/Models/Article.phpへ、登録可能なカラムと日時カラムの型を設定します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    protected $fillable = [
        'title',
        'category',
        'status',
        'publish_at',
        'published_at',
    ];

    protected function casts(): array
    {
        return [
            'publish_at' => 'datetime',
            'published_at' => 'datetime',
        ];
    }
}

MigrationとModelを用意したら、Migrationを実行します。

php artisan migrate

動作確認用の記事データを登録する

動作確認では、公開日時を過ぎた記事、未来の公開日時の記事、下書きの記事を用意します。例えば、次のような状態です。

タイトル カテゴリ ステータス 公開日時
PHPの配列を扱う tech scheduled 現在時刻より前
Laravelのログを確認する tech scheduled 現在時刻より後
お知らせ news scheduled 現在時刻より前
下書き記事 tech draft 現在時刻より前

例えば、Seederへ次のように記述できます。

<?php

namespace Database\Seeders;

use App\Models\Article;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        Article::query()->create([
            'title' => 'PHPの配列を扱う',
            'category' => 'tech',
            'status' => 'scheduled',
            'publish_at' => now()->subMinutes(10),
        ]);

        Article::query()->create([
            'title' => 'Laravelのログを確認する',
            'category' => 'tech',
            'status' => 'scheduled',
            'publish_at' => now()->addDay(),
        ]);

        Article::query()->create([
            'title' => 'お知らせ',
            'category' => 'news',
            'status' => 'scheduled',
            'publish_at' => now()->subMinutes(5),
        ]);

        Article::query()->create([
            'title' => '下書き記事',
            'category' => 'tech',
            'status' => 'draft',
            'publish_at' => now()->subMinutes(5),
        ]);
    }
}

新しく作った確認用プロジェクトであれば、次のコマンドでSeederを実行できます。

php artisan db:seed

既存のデータベースで実行する場合は、既存データを削除するコマンドを安易に使わず、対象環境とMigrationの内容を確認してから実行してください。

公開処理用のコマンドを作成する

ここからは、先ほど確認した一般的な作り方を使って、公開処理用のコマンドを作成します。

php artisan make:command PublishScheduledArticles

通常は、app/Console/Commands/PublishScheduledArticles.phpが作成されます。

今回のオプションを確認する

今回のコマンドでは、--categoryで対象カテゴリを絞り込み、--dry-runで更新前の確認だけを行います。シグネチャは、先ほど説明した属性記法で次のように定義します。

use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;

#[Signature('articles:publish {--category= : 公開対象のカテゴリ} {--dry-run : 更新せず対象だけ確認する}')]
#[Description('公開日時を過ぎた記事を公開する')]

コマンドを作成してこの定義を記述したら、ヘルプで入力方法を確認できます。

php artisan help articles:publish

公開処理を実装する

PublishScheduledArticlesへ、対象記事の検索、dry-run時の表示、本実行時の更新を記述します。

<?php

namespace App\Console\Commands;

use App\Models\Article;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;

#[Signature('articles:publish {--category= : 公開対象のカテゴリ} {--dry-run : 更新せず対象だけ確認する}')]
#[Description('公開日時を過ぎた記事を公開する')]
class PublishScheduledArticles extends Command
{
    public function handle(): int
    {
        $now = now();
        $category = $this->option('category');

        $targetQuery = Article::query()
            ->where('status', 'scheduled')
            ->whereNotNull('publish_at')
            ->where('publish_at', '<=', $now)
            ->when(
                $category !== null && $category !== '',
                function (Builder $query) use ($category): void {
                    $query->where('category', $category);
                }
            );

        $articles = (clone $targetQuery)
            ->orderBy('id')
            ->get(['id', 'title', 'category', 'publish_at']);

        if ($articles->isEmpty()) {
            $this->info('公開対象の記事はありません。');

            return self::SUCCESS;
        }

        $this->info("公開対象: {$articles->count()}件");

        foreach ($articles as $article) {
            $publishAt = $article->publish_at?->toDateTimeString() ?? '未設定';

            $this->line(sprintf(
                '#%d [%s] %s(公開日時: %s)',
                $article->id,
                $article->category,
                $article->title,
                $publishAt,
            ));
        }

        if ($this->option('dry-run')) {
            $this->comment('dry-runのため、データベースは更新していません。');

            return self::SUCCESS;
        }

        $updated = $targetQuery->update([
            'status' => 'published',
            'published_at' => $now,
            'updated_at' => $now,
        ]);

        $this->info("{$updated}件を公開しました。");

        return self::SUCCESS;
    }
}

対象を検索する条件は、$targetQueryへまとめています。dry-runではこのクエリから対象を取得して表示し、本実行では同じ条件のクエリに対してupdateを実行します。

表示用の取得にcloneを使っているのは、一覧表示側にorderByを追加しても、更新側のクエリへ不要な並び順を持ち込まないためです。

dry-runで対象を確認する

まず、dry-runを付けて実行します。

php artisan articles:publish --dry-run

出力は、例えば次のようになります。

公開対象: 2件
#1 [tech] PHPの配列を扱う(公開日時: 2026-09-10 09:00:00)
#3 [news] お知らせ(公開日時: 2026-09-10 09:05:00)
dry-runのため、データベースは更新していません。

この時点では、データベースのstatusはscheduledのままです。対象件数やタイトルを確認し、条件が意図どおりであることを確かめてから、本実行へ進みます。

カテゴリを指定した場合は、指定したカテゴリの記事だけが対象になります。

php artisan articles:publish --category=tech --dry-run

dry-runの分岐より前にupdateを実行しないことが重要です。検索や表示の処理の中で、意図せずModelの保存処理を呼び出さないようにします。

本実行で公開ステータスを変更する

dry-runの結果を確認したら、オプションを付けずに本実行します。

php artisan articles:publish

対象の記事について、次の値が更新されます。

  • status: scheduledからpublished
  • published_at: コマンド実行時刻
  • updated_at: コマンド実行時刻

status = scheduledという条件を付けているため、本実行後に同じコマンドを実行しても、公開済みの記事は再び対象になりません。状態を次へ進める条件を検索条件へ含めておくと、再実行時の意図しない更新を抑えられます。

入力値と終了コードを確認する

Artisanコマンドは、ターミナルから人が実行するだけでなく、スクリプトやスケジューラーから実行する場合もあります。そのため、画面へ表示するメッセージと、プロセスの終了コードを分けて考えます。

  • 正常終了: 0
  • 入力エラーや処理失敗: 0以外
  • 対象なし: この例では正常終了として0

この例では、正常終了時にself::SUCCESSを返しています。DB接続エラーなどで例外が発生した場合は、Laravel側でエラーとして終了します。入力値を独自に検査する場合は、errorでメッセージを表示し、失敗を表す終了コードを返します。

$category = $this->option('category');

if ($category !== null && ! preg_match('/^[a-z0-9_-]+$/', $category)) {
    $this->error('カテゴリの形式が正しくありません。');

    return self::FAILURE;
}

入力値の検証内容は、実際のカテゴリの仕様に合わせて決めてください。当然ですが上記の正規表現を、どのプロジェクトでもそのまま使えるわけではありません。

Console Testで実行結果を確認する

コマンドの動作は、手動実行だけでなく、Console Testで確認できます。次のコマンドでテストファイルを作成します。

php artisan make:test PublishScheduledArticlesTest

テストでは、dry-runでDBが変更されないことと、本実行で対象の記事だけが更新されることを確認します。

<?php

namespace Tests\Feature;

use App\Models\Article;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PublishScheduledArticlesTest extends TestCase
{
    use RefreshDatabase;

    public function test_dry_run_does_not_update_articles(): void
    {
        Article::query()->create([
            'title' => '公開予定の記事',
            'category' => 'tech',
            'status' => 'scheduled',
            'publish_at' => now()->subMinute(),
        ]);

        $this->artisan('articles:publish --dry-run')
            ->assertExitCode(0);

        $this->assertDatabaseHas('articles', [
            'title' => '公開予定の記事',
            'status' => 'scheduled',
            'published_at' => null,
        ]);
    }

    public function test_publishes_scheduled_articles(): void
    {
        Article::query()->create([
            'title' => '公開する記事',
            'category' => 'tech',
            'status' => 'scheduled',
            'publish_at' => now()->subMinute(),
        ]);

        $this->artisan('articles:publish')
            ->assertExitCode(0);

        $this->assertDatabaseHas('articles', [
            'title' => '公開する記事',
            'status' => 'published',
        ]);
    }
}

Laravel 13.xでは、Artisanコマンドの入力、出力、終了コードをテストするためのConsole Tests公式ドキュメントが用意されています。

スケジューラーから定期実行する

Artisanコマンドを定期的に実行する場合は、Laravelのタスクスケジューラーへ登録できます。Laravel 13.xでは、例えばroutes/console.phpへ次のように記述します。

<?php

use Illuminate\Support\Facades\Schedule;

Schedule::command('articles:publish')
    ->everyMinute()
    ->withoutOverlapping();

withoutOverlappingを付けると、前回の処理が終わっていない間に、次の処理が重ならないようにできます。利用するキャッシュドライバーや複数サーバー構成によって、必要な設定は変わります。

サーバー側では、cronからLaravelのschedule:runを毎分実行します。

* * * * * cd /var/www/example.com/current && /usr/bin/php artisan schedule:run >> /dev/null 2>&1

Laravelのタスクスケジューラーでは、アプリケーション内へスケジュールを定義し、サーバー側には通常一つのcronエントリを用意します。詳しくはLaravel 13.xのTask Scheduling公式ドキュメントを参照してください。

cronからArtisanコマンドを直接何本も起動する方法もありますが、スケジュールが増えると管理場所が分散します。まずはLaravelのスケジューラーへ集約し、必要な場合だけサーバー固有のcron設定を追加すると整理しやすくなります。

実装時に注意すること

dry-runはロールバックではない

今回のdry-runは、更新処理を実行せずに対象を表示する仕組みです。DB更新を実行してから元へ戻すロールバック方式ではありません。

dry-runの表示と本実行の間に、別の処理が記事を更新する可能性もあります。重要な処理では、実行時間を短くする、排他制御を使う、更新前にもう一度条件を確認するなど、要件に応じた対策が必要です。

日時とタイムゾーンをそろえる

publish_atを保存するタイムゾーンと、now()が返す時刻の扱いがずれていると、予定より早く公開されたり、遅れて公開されたりします。アプリケーション、データベース、サーバーのタイムゾーンを確認し、記事の入力画面でも利用者へ基準となるタイムゾーンを伝えます。

大量データを一度に読み込まない

今回の例では、対象記事を取得して一覧表示するためにget()を使っています。対象件数が多い場合は、すべてのレコードをメモリへ読み込む構成を見直し、chunkByIdやカーソルを使った処理を検討します。

大量更新では、1回の処理件数、処理時間、途中失敗時の再実行方法、ログの出し方まで決めておく必要があります。初心者向けのサンプルをそのまま大量データの本番処理へ使わないようにしてください。

接続先データベースを確認する

DB更新を行うコマンドは、接続先を間違えると意図しない環境のデータを変更します。ローカル、ステージング、本番の環境変数と設定キャッシュを確認し、初回はdry-runの出力と対象件数を確認してから実行します。

記事公開のような処理であっても、実行ユーザー、ログの保存先、失敗時の通知、バックアップの有無を確認しておくと、問題が起きたときに切り分けやすくなります。

まとめ

今回は、Laravel 13で日時指定された記事を公開するArtisanコマンドを作成しました。

  • make:commandでカスタムコマンドを作成する
  • Signature属性またはプロパティで引数・オプションを定義する
  • 公開日時とステータスを条件にして、対象記事を検索する
  • --dry-runで対象を確認してから本実行する
  • 終了コードを返し、スクリプトやスケジューラーから結果を判定できるようにする
  • 定期実行では、タイムゾーン、重複実行、DB接続先、ログを確認する

Artisanコマンドは、記事の公開処理だけでなく、データ補正、定期的な集計、古いデータの整理などにも利用できます。DBを更新するコマンドを作るときは、処理を書くことだけでなく、実行前に対象を確認できることと、再実行しても問題が起きにくいことまで考えるとよいかと思います。

参考資料

この記事を書いた人

※上が私です。

奈良市を拠点に、27年以上の経験を持つフリーランスWebエンジニア、阿部辰也です。

これまで、ECサイトのバックエンド開発や業務効率化システム、公共施設の予約システムなど、多彩なプロジェクトを手がけ、企業様や制作会社様のパートナーとして信頼を築いてまいりました。

【制作会社・企業様向けサポート】
  • 専任エンジニアのいない企業様に対するシステム面の不安を解消
  • 柔軟な契約形態や短納期での対応により、急なニーズにも迅速にサポート
  • システムの企画段階から運用まで、ワンストップでのサービスを提供

Webシステムの開発やサイト改善でお困りの際は、どうぞお気軽にご相談ください。小さな疑問から大規模プロジェクトまで、最適なご提案を心を込めてさせていただきます。

ぜひ、プロフィールやWeb制作会社様向け業務案内、一般企業様向け業務案内もご覧くださいね。

Laravelのコードレビューで遅いSQLの候補を洗い出す

2026.10.02

Laravelのコードレビューで、ループ内のDBアクセスやEloquentのN+1、大量取得、検索条件とインデックスなど、性能問題につながりやすい箇所を候補として洗い出します。Laravelで発行SQLと実行時間を確認し、MySQLのEXPLAINや実データに近い環境での計測へつなげる方法を紹介します。コードレビューだけではSQLの実行時間を断定できない点も説明します。

Laravel MySQL PHP

Windows 11でDocker Composeを使ってLaravel環境を作る:Container・Volume・Networkの基本

2026.10.01

Windows 11のDocker Desktop上に、DockerfileとDocker Composeを使ってLaravel・PHP・MySQL・Mailpitの開発環境を構築します。ImageとContainerの関係、Bind MountとNamed Volumeによるデータの保存、ポート公開とContainer間通信の違いを実例で確認。LaravelからMySQLへ接続するときに、localhostではなくService名を使う理由も説明します。

Docker Laravel PHP

LaravelのArtisanコマンドを本番運用する:冪等性・二重実行防止・再実行の設計

2026.09.22

LaravelのArtisanコマンドでデータベースを更新する処理を、本番環境で安全に運用するための考え方を整理します。商品在庫の一括更新を例に、冪等性、dry-run、チャンク処理、トランザクション、二重実行防止、失敗後の再実行、ログや終了コード、実行前後の確認手順を紹介します。

Laravel PHP

Laravel 13のNamed Rate Limiterでレート制限を実装する:IP・ユーザー・入力値ごとの設定

2026.09.19

LaravelのNamed Rate Limiterを使い、無制限に繰り返されたくない処理へレート制限を追加する方法を解説します。RateLimiter::for()とthrottleミドルウェアの基本から、IPアドレス・ユーザーID・入力値による制限、複数の制限、429レスポンス、ログイン処理への応用、Redis利用時の注意点まで整理します。

Laravel PHP

阿部辰也へのお仕事の依頼・お問い合わせ

軽いご相談もお気軽にどうぞ!

個人情報の取り扱いについて *必須 プライバシーポリシーをご確認いただき、同意いただける場合は「同意する」にチェックをしてください。

keyboard_double_arrow_up
TOP