404 Not Found

404 Not Found


nginx

Laravel Artisanコンソールとカスタムコマンド

ArtisanはLaravelの「スイスアーミーナイフ」です—100以上の組み込みコマンドがすべての操作・保守タスクをカバーし, カスタムコマンドで反復作業をワンクリックで処理できます。

1. 学ぶこと


2. 運用エンジニアの実話

(1) 痛み:毎日20の運用タスクを手動で実行している

毎朝, Charlieは次のタスクを手動で実行しなければなりません:期限切れセッションのクリア, 日次レポートの生成, サブスクリプションステータスの同期, 期限切れリマインドメールの送信, データベースのバックアップ...5つのターミナルウィンドウに分散された20のタスク—1つでも忘れると大変です。Bobは言いました。「これって全部スケジュールタスクじゃないの?なんで手動でやってるの?」

(2) Artisanコマンドとスケジューリングによる解決策

Artisanカスタムコマンドは反復タスクを1つのコマンドにまとめ, Scheduleが自動的にスケジュール実行します—Charlieは毎日1回実行ログを確認するだけで済みます。

BASH
# 1つのコマンドで全て実行
php artisan shopmetrics:daily-maintenance

# またはスケジューラーに午前2時に自動実行させる
php artisan schedule:run

(3) 成果

CharlieがArtisanでスケジューリングを設定した後, 20のタスクがすべて自動実行され, 失敗率は5%から0%に低下しました。


3. 組み込みコマンドの完全一覧

(1) 主要コマンドのカテゴリ

カテゴリ コマンド 説明
アプリケーション about 環境情報の概要
down / up メンテナンスモード切替
env .env設定の表示
データベース migrate マイグレーション実行
migrate:rollback マイグレーションロールバック
migrate:fresh データベース再構築
db:seed データ投入
db:show データベース情報
キャッシュ cache:clear キャッシュクリア
config:cache / clear 設定キャッシュ
route:cache / clear ルートキャッシュ
view:cache / clear ビューキャッシュ
ルート route:list 全ルートの一覧表示
キュー queue:work ワーカー起動
queue:failed 失敗タスクの確認
queue:retry 失敗タスクのリトライ
生成 make:model Modelの作成
make:controller コントローラーの作成
make:migration マイグレーションの作成
make:command コマンドの作成

(1) ▶ サンプル:主要Artisanコマンドクイックリファレンス

BASH
# アプリ情報
php artisan about
php artisan env

# データベース操作
php artisan migrate --seed
php artisan db:show --counts

# キャッシュ管理
php artisan cache:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize           # 設定 + ルート + ビューのキャッシュ

# メンテナンスモード
php artisan down --secret="maintenance-token"  # ?secret=でアクセス許可
php artisan up

# キュー管理
php artisan queue:work --queue=high,default
php artisan queue:failed
php artisan queue:retry all

# ルート確認
php artisan route:list --path=api
php artisan route:list --columns=method,uri,name

出力:

TEXT
# コマンド実行成功

4. カスタムコマンド

(1) コマンドの作成

BASH
php artisan make:command ProcessTenantAnalytics
# 作成される:app/Console/Commands/ProcessTenantAnalytics.php

(2) コマンドSignature構文

PHP
// app/Console/Commands/ProcessTenantAnalytics.php
class ProcessTenantAnalytics extends Command
{
    // Signature構文:{argument} {--option}
    protected $signature = 'shopmetrics:analytics
                            {tenant? : Tenant ID or slug (optional)}
                            {--type=monthly : Report type (daily|weekly|monthly)}
                            {--force : Force re-calculation}
                            {--format=csv : Output format (csv|json)}';

    protected $description = 'Process analytics for tenants';

    public function handle(): int
    {
        $tenant = $this->argument('tenant');
        $type = $this->option('type');
        $force = $this->option('force');

        if ($tenant) {
            $this->processSingleTenant($tenant, $type, $force);
        } else {
            $this->processAllTenants($type, $force);
        }

        return self::SUCCESS;
    }
}

(3) Signature構文ルール

構文 説明
{name} 必須引数 {tenant}
{name?} オプション引数 {tenant?}
{name=default} デフォルト値付き {type=monthly}
{--option} ブールオプション --force
{--option=default} 値付きオプション --format=csv
{--O|shortcut} 短縮オプション --force|f

(1) ▶ サンプル:ShopMetricsテナント管理コマンド

PHP
// app/Console/Commands/ManageTenant.php
class ManageTenant extends Command
{
    protected $signature = 'shopmetrics:tenant
                            {action : Action to perform (list|suspend|activate|stats)}
                            {tenant? : Tenant ID or slug}
                            {--with-users : Include user statistics}';

    protected $description = 'Manage ShopMetrics tenants';

    public function handle(): int
    {
        $action = $this->argument('action');

        match ($action) {
            'list' => $this->listTenants(),
            'suspend' => $this->suspendTenant(),
            'activate' => $this->activateTenant(),
            'stats' => $this->showTenantStats(),
            default => $this->error("Unknown action: {$action}"),
        };

        return self::SUCCESS;
    }

    private function listTenants(): void
    {
        $tenants = Tenant::withCount(['shops', 'users'])->get();
        $this->table(
            ['ID', 'Name', 'Slug', 'Status', 'Shops', 'Users'],
            $tenants->map(fn ($t) => [
                $t->id, $t->name, $t->slug, $t->status,
                $t->shops_count, $t->users_count,
            ])
        );
    }

    private function suspendTenant(): void
    {
        $identifier = $this->argument('tenant') ?? $this->ask('Enter tenant ID or slug:');
        $tenant = $this->resolveTenant($identifier);
        $tenant->update(['status' => 'suspended']);
        $this->info("Tenant {$tenant->name} has been suspended.");
    }

    private function resolveTenant(string $identifier): Tenant
    {
        return is_numeric($identifier)
            ? Tenant::findOrFail($identifier)
            : Tenant::whereSlug($identifier)->firstOrFail();
    }
}

出力:

TEXT
// 実行成功

5. コマンドの引数とオプション

(1) パラメータの取得

PHP
// 引数
$name = $this->argument('name');        // 単一引数
$all = $this->arguments();              // 全引数を配列で取得

// オプション
$force = $this->option('force');        // 単一オプション (ブールまたは値)
$all = $this->options();                // 全オプションを配列で取得

(2) 配列パラメータ

PHP
// 配列引数付きのSignature
protected $signature = 'shopmetrics:report
                        {tenants* : One or more tenant IDs}
                        {--type=monthly}';

// 使用方法
php artisan shopmetrics:report 1 2 3 --type=weekly

// アクセス
$tenants = $this->argument('tenants'); // [1, 2, 3]

(3) 出力メソッド

メソッド 説明 サンプル出力
info() 緑の情報 ✓ Done
error() 赤のエラー ✗ Failed
warn() 黄の警告 ⚠ Warning
line() プレーンテキスト プレーンテキスト
table() テーブル フォーマットテーブル
progressBar() プログレスバー ██████░░ 60%

(1) ▶ サンプル:プログレスバー付きShopMetricsデータクリーニングコマンド

PHP
// app/Console/Commands/CleanupExpiredData.php
class CleanupExpiredData extends Command
{
    protected $signature = 'shopmetrics:cleanup
                            {--days=90 : Delete data older than N days}
                            {--dry-run : Show what would be deleted}';

    protected $description = 'Clean up expired data (old reports, expired tokens)';

    public function handle(): int
    {
        $days = $this->option('days');
        $dryRun = $this->option('dry-run');
        $cutoff = now()->subDays($days);

        // 期限切れトークンのクリーニング
        $expiredTokens = Sanctum::$personalAccessTokenModel::where('last_used_at', '<', $cutoff);
        $this->info(($dryRun ? 'Would delete' : 'Deleting') . " {$expiredTokens->count()} expired tokens.");

        // S3の古いレポートのクリーニング
        $bar = $this->output->createProgressBar(Tenant::count());
        $deletedFiles = 0;

        Tenant::chunk(100, function ($tenants) use ($cutoff, $dryRun, &$deletedFiles, $bar) {
            foreach ($tenants as $tenant) {
                $files = Storage::disk('s3')->allFiles("reports/{$tenant->slug}");
                foreach ($files as $file) {
                    if (Storage::disk('s3')->lastModified($file) < $cutoff->timestamp) {
                        if (!$dryRun) Storage::disk('s3')->delete($file);
                        $deletedFiles++;
                    }
                }
                $bar->advance();
            }
        });

        $bar->finish();
        $this->newLine();
        $this->info(($dryRun ? 'Would delete' : 'Deleted') . " {$deletedFiles} old report files.");

        if (!$dryRun) {
            $expiredTokens->delete();
        }

        return self::SUCCESS;
    }
}

出力:

TEXT
// 実行成功

6. コマンドスケジューリング

(1) スケジューリングの定義

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

Schedule::command('shopmetrics:analytics --type=daily')
    ->dailyAt('02:00')
    ->onOneServer()
    ->withoutOverlapping()
    ->emailOutputOnFailure('admin@shopmetrics.io');

Schedule::command('shopmetrics:cleanup --days=90')
    ->weekly()
    ->sundays()
    ->at('03:00')
    ->onOneServer();

Schedule::command('shopmetrics:sync-subscriptions')
    ->dailyAt('06:00')
    ->onOneServer()
    ->withoutOverlapping();

Schedule::job(new ProcessMonthlyAnalyticsJob)
    ->monthlyOn(1, '00:00')
    ->onOneServer();

(2) スケジューリング頻度オプション

メソッド 頻度 同等のCron
everyMinute() 毎分 * * * * *
everyFiveMinutes() 5分ごと */5 * * * *
hourly() 毎時 0 * * * *
daily() 毎日午前12時 0 0 * * *
dailyAt('14:00') 毎日午後2時 0 14 * * *
weekly() 毎週日曜日の深夜 0 0 * * 0
monthly() 毎月1日の深夜 0 0 1 * *
cron('...') カスタムCron 任意

(3) スケジューリング制約

メソッド 説明
onOneServer() 1台のサーバーのみで実行
withoutOverlapping() 重複実行を禁止
runInBackground() バックグラウンドで実行
when(Closure) 条件付き実行
environments('prod') 指定環境のみ
emailOutputOnFailure() 失敗時にメール通知

(1) ▶ サンプル:ShopMetricsスケジューリングの完全設定

PHP
// routes/console.php
Schedule::command('shopmetrics:analytics --type=daily')
    ->dailyAt('02:00')
    ->onOneServer()
    ->withoutOverlapping(60)
    ->emailOutputOnFailure('ops@shopmetrics.io');

Schedule::command('shopmetrics:cleanup --days=90')
    ->weeklyOn(Schedule::SUNDAY, '03:00')
    ->onOneServer();

Schedule::command('shopmetrics:send-expiring-notifications')
    ->dailyAt('08:00')
    ->when(fn () => Subscription::expiringSoon()->exists());

Schedule::command('queue:prune-failed --hours=168')
    ->daily();

Schedule::command('queue:prune-batches --hours=168')
    ->daily();

// サーバーのCronエントリ
// * * * * * cd /var/www/shopmetrics && php artisan schedule:run >> /dev/null 2>&1

出力:

TEXT
// 実行成功

7. インタラクティブコマンド

(1) インタラクションメソッド

PHP
// 入力の要求
$name = $this->ask('What is the tenant name?');

// デフォルト値付きの入力要求
$email = $this->ask('Email address?', 'admin@example.com');

// シークレット入力 (パスワード)
$password = $this->secret('Enter password:');

// 確認 (yes/no)
if ($this->confirm('Do you wish to continue?', true)) {
    // デフォルト:はい
}

// 選択 (単一選択)
$type = $this->choice(
    'Select report type',
    ['daily', 'weekly', 'monthly'],
    0 // デフォルトインデックス
);

// 自動補完
$name = $this->anticipate('Tenant name', Tenant::pluck('name')->toArray());

(2) 総合インタラクションワークフロー

PHP
// app/Console/Commands/SetupTenant.php
class SetupTenant extends Command
{
    protected $signature = 'shopmetrics:tenant-setup';

    protected $description = 'Interactive tenant setup wizard';

    public function handle(): int
    {
        $this->info('=== ShopMetrics Tenant Setup Wizard ===');

        $name = $this->ask('Tenant name');
        $slug = $this->anticipate('Slug', [Str::slug($name)]);
        $domain = $this->ask('Custom domain (optional)', $slug . '.shopmetrics.io');
        $plan = $this->choice('Select plan', ['Starter', 'Pro', 'Enterprise'], 1);
        $ownerEmail = $this->ask('Owner email');

        $this->table(
            ['Field', 'Value'],
            [['Name', $name], ['Slug', $slug], ['Domain', $domain], ['Plan', $plan], ['Owner', $ownerEmail]],
        );

        if (!$this->confirm('Create this tenant?', true)) {
            $this->warn('Cancelled.');
            return self::FAILURE;
        }

        $tenant = Tenant::create(compact('name', 'slug', 'domain'));
        User::factory()->create([
            'tenant_id' => $tenant->id,
            'email' => $ownerEmail,
            'role' => 'tenant_owner',
        ]);

        $this->info("Tenant {$name} created successfully!");
        return self::SUCCESS;
    }
}

(1) ▶ サンプル:ShopMetricsインタラクティブデータエクスポートコマンド

PHP
// app/Console/Commands/ExportData.php
class ExportData extends Command
{
    protected $signature = 'shopmetrics:export';

    protected $description = 'Interactive data export tool';

    public function handle(): int
    {
        $type = $this->choice('What to export?', [
            'orders' => 'Orders',
            'products' => 'Products',
            'analytics' => 'Analytics Report',
        ]);

        $tenant = $this->anticipate('Tenant (leave blank for all)', Tenant::pluck('name')->push('All')->toArray());

        $format = $this->choice('Format?', ['csv', 'xlsx', 'json'], 0);

        $dateFrom = $this->ask('Date from (Y-m-d, optional)');
        $dateTo = $this->ask('Date to (Y-m-d, optional)');

        $this->info("Exporting {$type} for {$tenant} in {$format} format...");

        $query = match ($type) {
            'orders' => Order::query(),
            'products' => Product::query(),
            'analytics' => AnalyticsReport::query(),
        };

        if ($tenant !== 'All') {
            $tenantModel = Tenant::whereName($tenant)->firstOrFail();
            $query->where('tenant_id', $tenantModel->id);
        }

        if ($dateFrom) $query->where('created_at', '>=', $dateFrom);
        if ($dateTo) $query->where('created_at', '<=', $dateTo);

        $count = $query->count();
        $this->info("Found {$count} records.");

        if (!$this->confirm("Export {$count} records?", true)) {
            return self::FAILURE;
        }

        $path = ExportService::export($query, $format);
        $this->info("Export saved to: {$path}");

        return self::SUCCESS;
    }
}

出力:

TEXT
// 実行成功

8. 総合サンプル:ShopMetrics運用コマンドセット

PHP
// ============================================
// 総合:ShopMetrics運用コマンド
// 対象:signature, options, progress, scheduling, interactive
// ============================================

// app/Console/Commands/DailyMaintenance.php
class DailyMaintenance extends Command
{
    protected $signature = 'shopmetrics:daily-maintenance
                            {--skip-analytics : Skip analytics processing}
                            {--skip-cleanup : Skip data cleanup}
                            {--notify : Send completion notification}';

    protected $description = 'Run daily maintenance tasks';

    public function handle(): int
    {
        $this->info('Starting daily maintenance...');

        if (!$this->option('skip-analytics')) {
            $this->processAnalytics();
        }

        if (!$this->option('skip-cleanup')) {
            $this->cleanupExpiredData();
        }

        $this->syncSubscriptions();

        if ($this->option('notify')) {
            $this->sendCompletionNotification();
        }

        $this->info('Daily maintenance completed.');
        return self::SUCCESS;
    }

    private function processAnalytics(): void
    {
        $this->info('Processing daily analytics...');
        $tenants = Tenant::active()->get();
        $bar = $this->output->createProgressBar($tenants->count());

        foreach ($tenants as $tenant) {
            GenerateReportJob::dispatch($tenant, 'daily', 'json');
            $bar->advance();
        }

        $bar->finish();
        $this->newLine();
    }

    private function cleanupExpiredData(): void
    {
        $this->call('shopmetrics:cleanup', ['--days' => 90]);
    }

    private function syncSubscriptions(): void
    {
        $this->call('shopmetrics:sync-subscriptions');
    }

    private function sendCompletionNotification(): void
    {
        $admin = User::where('role', 'super_admin')->first();
        $admin?->notify(new DailyMaintenanceCompleted());
    }
}

// スケジュール設定
// routes/console.php
Schedule::command('shopmetrics:daily-maintenance --notify')
    ->dailyAt('02:00')
    ->onOneServer()
    ->withoutOverlapping()
    ->emailOutputOnFailure('ops@shopmetrics.io');

❓ よくある質問

Q make:commandmake:command --commandの違いは何ですか?
A --command=xxxでカスタムコマンド名を指定します:php artisan make:command SendEmails --command=emails:send。省略した場合, デフォルト名app:console-commands:send-emailsが使用されます。常にカスタムコマンド名を指定することをお勧めします。
Q ジョブスケジューリングにはCronとLaravel Scheduleのどちらを使うべきですか?
A Laravel Scheduleを使用してください。サーバーにはCronジョブ1つだけが必要です:* * * * * php artisan schedule:run。すべてのタスクがコード内で定義されるため, バージョン管理とテストが可能です。各タスクに個別のCronジョブを設定しないでください。
Q onOneServerはいつ使用すべきですか?
A 複数サーバーにデプロイしている場合, 同じスケジュールタスクが複数のマシンで同時に実行されるのを防ぐためです。Redisまたはデータベースキャッシュドライバーが調整に必要です。単一サーバーでは不要です。
Q カスタムコマンドをテストするにはどうすればよいですか?
A テスト内で$this->artisan('command:name', ['arg' => 'value'])でコマンドを実行し, 出力が->expectsOutput('Done')であることをアサートするか, データベースの変更を検証します。
Q コマンドが遅すぎる場合はどうすればよいですか?
A 時間のかかる操作はキューで非同期に処理し, コマンドはジョブのディスパッチのみを担当します。大量データセットにはチャンクとプログレスバーを使用してください。実行前に結果をプレビューするには--dry-runオプションを追加します。
Q コマンド出力をログに記録するにはどうすればよいですか?
A スケジューラーで->appendOutputTo(storage_path('logs/command.log'))を使用してログを追加します。コマンド内ではLog::info()を使用します。失敗時のメール通知にはemailOutputOnFailure()を使用します。

📖 まとめ


📝 練習問題

  1. 基本問題 (⭐):shopmetrics:tenant-statsという名前のコマンドを作成し, tenantパラメータを受け付け, そのテナントの店舗数, 注文数, 総売上を出力し, tableコマンドでフォーマットしてください。

  2. 応用問題 (⭐⭐):shopmetrics:daily-maintenanceコマンドを作成し, アナリティクス処理 (プログレスバー付き)+ 期限切れデータのクリーンアップ + サブスクリプション同期を含め, Scheduleを設定して毎日午前2時に自動実行してください。

  3. チャレンジ (⭐⭐⭐):インタラクティブなshopmetrics:tenant-setupウィザードコマンドを実装してください—テナント名, slug, プラン, 管理者メールアドレスを順番に入力し, 各ステップでバリデーションを行い, 最後に作成を確認します。テナント名には自動補完 (anticipate)の候補を含めてください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%