Laravel: Laravel Artisan控制台与自定义命令

最后更新:2026-08-26

Artisan 是 Laravel 的"瑞士军刀"——内置 100+ 命令覆盖所有运维操作,自定义命令让重复任务一键搞定。

1. 你将学到


2. 一个运维工程师的真实故事

(1) 痛点:每天手动执行 20 个运维任务

Charlie 每天早上要手动执行:清过期 Session、生成日度报表、同步订阅状态、发送到期提醒邮件、备份数据库……20 个任务分散在 5 个终端窗口,漏了一个就要赔钱。Bob 说:"这些不都是定时任务吗?为什么要手动?"

(2) Artisan 命令与调度的解法

Artisan 自定义命令把重复操作封装成一行命令,Schedule 自动定时执行——Charlie 每天只需看一眼执行日志。

BASH
# One command does everything
php artisan shopmetrics:daily-maintenance

# Or let the scheduler run it automatically at 2 AM
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 启动 Worker
queue:failed 查看失败任务
queue:retry 重试失败任务
生成 make:model 创建模型
make:controller 创建控制器
make:migration 创建迁移
make:command 创建命令

▶ 示例:常用 Artisan 命令速查

BASH
# App info
php artisan about
php artisan env

# Database operations
php artisan migrate --seed
php artisan db:show --counts

# Cache management
php artisan cache:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize           # Cache config + route + view

# Maintenance mode
php artisan down --secret="maintenance-token"  # Allow access with ?secret=
php artisan up

# Queue management
php artisan queue:work --queue=high,default
php artisan queue:failed
php artisan queue:retry all

# Route inspection
php artisan route:list --path=api
php artisan route:list --columns=method,uri,name

输出:

TEXT 📖 仅展示
# 命令执行成功

4. 自定义命令

(1) 创建命令

BASH
php artisan make:command ProcessTenantAnalytics
# Creates: app/Console/Commands/ProcessTenantAnalytics.php

(2) 命令签名语法

PHP
// app/Console/Commands/ProcessTenantAnalytics.php
class ProcessTenantAnalytics extends Command
{
    // Signature syntax: {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) 签名语法规则

语法 说明 示例
{name} 必选参数 {tenant}
{name?} 可选参数 {tenant?}
{name=default} 带默认值 {type=monthly}
{--option} 布尔选项 --force
{--option=default} 带值选项 --format=csv
{--O|shortcut} 短选项 --force|f

▶ 示例: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
// Arguments
$name = $this->argument('name');        // Single argument
$all = $this->arguments();              // All arguments as array

// Options
$force = $this->option('force');        // Single option (boolean or value)
$all = $this->options();                // All options as array

(2) 数组参数

PHP
// Signature with array argument
protected $signature = 'shopmetrics:report
                        {tenants* : One or more tenant IDs}
                        {--type=monthly}';

// Usage
php artisan shopmetrics:report 1 2 3 --type=weekly

// Access
$tenants = $this->argument('tenants'); // [1, 2, 3]

(3) 输出方法

方法 说明 示例输出
info() 绿色信息 ✓ Done
error() 红色错误 ✗ Failed
warn() 黄色警告 ⚠ Warning
line() 纯文本 Plain text
table() 表格 格式化表格
progressBar() 进度条 ██████░░ 60%

▶ 示例: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);

        // Clean expired tokens
        $expiredTokens = Sanctum::$personalAccessTokenModel::where('last_used_at', '<', $cutoff);
        $this->info(($dryRun ? 'Would delete' : 'Deleting') . " {$expiredTokens->count()} expired tokens.");

        // Clean old reports from 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() 每天 0 点 0 0 * * *
dailyAt('14:00') 每天 14:00 0 14 * * *
weekly() 每周日 0 点 0 0 * * 0
monthly() 每月 1 日 0 点 0 0 1 * *
cron('...') 自定义 Cron 任意

(3) 调度约束

方法 说明
onOneServer() 只在一台服务器执行
withoutOverlapping() 不允许重叠执行
runInBackground() 后台执行
when(Closure) 条件执行
environments('prod') 指定环境
emailOutputOnFailure() 失败时邮件通知

▶ 示例: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 entry on server
// * * * * * cd /var/www/shopmetrics && php artisan schedule:run >> /dev/null 2>&1

输出:

TEXT 📖 仅展示
// 执行成功

7. 交互式命令

(1) 交互方法

PHP
// Ask for input
$name = $this->ask('What is the tenant name?');

// Ask with default
$email = $this->ask('Email address?', 'admin@example.com');

// Secret input (passwords)
$password = $this->secret('Enter password:');

// Confirm (yes/no)
if ($this->confirm('Do you wish to continue?', true)) {
    // Default: yes
}

// Choice (single select)
$type = $this->choice(
    'Select report type',
    ['daily', 'weekly', 'monthly'],
    0 // default index
);

// Anticipate (autocomplete)
$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;
    }
}

▶ 示例: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
// ============================================
// Comprehensive: ShopMetrics Operations Commands
// Covers: 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());
    }
}

// Schedule it
// routes/console.php
Schedule::command('shopmetrics:daily-maintenance --notify')
    ->dailyAt('02:00')
    ->onOneServer()
    ->withoutOverlapping()
    ->emailOutputOnFailure('ops@shopmetrics.io');

❓ 常见问题

Q make:command 和 make: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:* * * * * php artisan schedule:run,所有任务定义在代码中,版本可控、可测试。不要给每个任务加单独的 Cron。
Q onOneServer 什么时候用?
A 多台服务器部署时,防止同一调度任务在多台机器上同时执行。需要 Redis 或 database 缓存驱动来协调。单台服务器可以不用。
Q 如何测试自定义命令?
A$this->artisan('command:name', ['arg' => 'value']) 在测试中执行命令,断言输出:->expectsOutput('Done') 或检查数据库变更。
Q 命令太慢怎么办?
A 耗时操作用队列异步处理,命令只负责 dispatch Job;大数据集用 chunk + 进度条;加 --dry-run 选项先看结果再执行。
Q 命令输出怎么记录日志?
A->appendOutputTo(storage_path('logs/command.log')) 在调度中追加日志;或命令内用 Log::info();也可用 emailOutputOnFailure() 失败时邮件通知。

📖 小节


📝 作业

  1. 基础题(⭐):创建 shopmetrics:tenant-stats 命令,接受 tenant 参数,输出该租户的商店数/订单数/总收入,使用 table 格式化输出。

  2. 进阶题(⭐⭐):创建 shopmetrics:daily-maintenance 命令,包含 analytics 处理(带进度条)+ 过期数据清理 + 订阅同步,配置 Schedule 每天凌晨 2 点自动执行。

  3. 挑战题(⭐⭐⭐):实现交互式 shopmetrics:tenant-setup 向导命令——依次输入租户名/slug/计划/管理员邮箱,每步有验证,最后确认创建,包含 auto-complete(anticipate)租户名建议。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏