Laravel 13 的命令行系统提供了强大的 CLI 工具开发能力,本文介绍命令行开发的高级技术。

命令行架构

命令基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;

abstract class BaseCommand extends Command
{
protected int $batchSize = 100;
protected bool $useCache = true;
protected int $cacheTtl = 3600;

protected function startTimer(): void
{
$this->startTime = microtime(true);
$this->startMemory = memory_get_usage(true);
}

protected function stopTimer(): array
{
return [
'duration' => round((microtime(true) - $this->startTime) * 1000, 2) . 'ms',
'memory' => $this->formatBytes(memory_get_usage(true) - $this->startMemory),
'peak_memory' => $this->formatBytes(memory_get_peak_usage(true)),
];
}

protected function formatBytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$power = $bytes > 0 ? floor(log($bytes, 1024)) : 0;
return number_format($bytes / pow(1024, $power), 2) . ' ' . $units[$power];
}

protected function withProgressBar(iterable $items, callable $callback): void
{
$bar = $this->output->createProgressBar(count($items));

foreach ($items as $item) {
$callback($item);
$bar->advance();
}

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

protected function cacheResult(string $key, callable $callback)
{
if (!$this->useCache) {
return $callback();
}

return Cache::remember($key, $this->cacheTtl, $callback);
}

protected function log(string $message, string $level = 'info'): void
{
$timestamp = now()->format('Y-m-d H:i:s');
$this->line("[{$timestamp}] [{$level}] {$message}");
}
}

高级命令开发

批处理命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?php

namespace App\Console\Commands;

use App\Models\User;
use Illuminate\Support\Facades\Bus;

class BatchProcessCommand extends BaseCommand
{
protected $signature = 'process:batch
{model : Model to process}
{--batch=100 : Batch size}
{--queue=default : Queue name}
{--dry-run : Show what would be processed}';

protected $description = 'Process records in batches';

public function handle(): int
{
$this->startTimer();

$model = $this->argument('model');
$batchSize = (int) $this->option('batch');
$queue = $this->option('queue');
$dryRun = $this->option('dry-run');

$modelClass = $this->resolveModel($model);

$total = $modelClass::count();

$this->info("Processing {$total} {$model} records in batches of {$batchSize}");

if ($dryRun) {
$this->warn('Dry run mode - no changes will be made');
return self::SUCCESS;
}

$jobs = [];
$processed = 0;

$modelClass::chunk($batchSize, function ($records) use ($queue, &$jobs, &$processed) {
$jobs[] = new \App\Jobs\ProcessBatch($records);
$processed += count($records);
$this->info("Prepared batch: {$processed} records");
});

$batch = Bus::batch($jobs)
->name("Process {$model}")
->onQueue($queue)
->dispatch();

$this->info("Batch ID: {$batch->id}");

$stats = $this->stopTimer();
$this->info("Completed in {$stats['duration']}");

return self::SUCCESS;
}

protected function resolveModel(string $model): string
{
return match ($model) {
'user', 'users' => User::class,
default => "App\\Models\\{$model}",
};
}
}

交互式命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<?php

namespace App\Console\Commands;

use App\Models\User;
use App\Services\UserService;

class InteractiveCommand extends BaseCommand
{
protected $signature = 'user:manage';
protected $description = 'Interactive user management';

public function handle(UserService $service): int
{
$action = $this->choice(
'What would you like to do?',
['Create User', 'Update User', 'Delete User', 'List Users', 'Exit'],
0
);

return match ($action) {
'Create User' => $this->createUser($service),
'Update User' => $this->updateUser($service),
'Delete User' => $this->deleteUser($service),
'List Users' => $this->listUsers(),
'Exit' => $this->exit(),
default => self::FAILURE,
};
}

protected function createUser(UserService $service): int
{
$name = $this->ask('Enter user name');
$email = $this->ask('Enter user email');
$password = $this->secret('Enter password');

if (!$this->confirm("Create user {$name} ({$email})?")) {
$this->info('Cancelled');
return self::SUCCESS;
}

$user = $service->create([
'name' => $name,
'email' => $email,
'password' => $password,
]);

$this->info("User created with ID: {$user->id}");

return self::SUCCESS;
}

protected function updateUser(UserService $service): int
{
$userId = $this->ask('Enter user ID');

$user = User::find($userId);

if (!$user) {
$this->error('User not found');
return self::FAILURE;
}

$name = $this->ask('Enter new name', $user->name);
$email = $this->ask('Enter new email', $user->email);

$service->update($user, [
'name' => $name,
'email' => $email,
]);

$this->info('User updated');

return self::SUCCESS;
}

protected function deleteUser(UserService $service): int
{
$userId = $this->ask('Enter user ID');

$user = User::find($userId);

if (!$user) {
$this->error('User not found');
return self::FAILURE;
}

if (!$this->confirm("Delete user {$user->name}? This cannot be undone.")) {
$this->info('Cancelled');
return self::SUCCESS;
}

$service->delete($user);

$this->info('User deleted');

return self::SUCCESS;
}

protected function listUsers(): int
{
$users = User::select(['id', 'name', 'email', 'created_at'])
->orderBy('created_at', 'desc')
->limit(20)
->get();

$this->table(
['ID', 'Name', 'Email', 'Created'],
$users->map(fn($u) => [$u->id, $u->name, $u->email, $u->created_at->diffForHumans()])
);

return self::SUCCESS;
}

protected function exit(): int
{
$this->info('Goodbye!');
return self::SUCCESS;
}
}

命令调度

可调度命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Contracts\Scheduling\ShouldSchedule;

class ScheduledCleanupCommand extends Command implements ShouldSchedule
{
protected $signature = 'cleanup:expired';
protected $description = 'Clean up expired records';

public function handle(): int
{
$expired = \App\Models\Token::where('expires_at', '<', now())->delete();

$this->info("Deleted {$expired} expired tokens");

return self::SUCCESS;
}

public function schedule(): void
{
$this->daily()->at('03:00');
}
}

命令中间件

命令中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php

namespace App\Console\Middleware;

use Closure;

class WithoutOverlapping
{
protected string $key;
protected int $ttl;

public function __construct(string $key, int $ttl = 3600)
{
$this->key = $key;
$this->ttl = $ttl;
}

public function handle($command, Closure $next)
{
$lockKey = 'command:lock:' . $this->key;

if (\Illuminate\Support\Facades\Cache::has($lockKey)) {
$command->warn('Command is already running');
return;
}

\Illuminate\Support\Facades\Cache::put($lockKey, true, $this->ttl);

try {
return $next($command);
} finally {
\Illuminate\Support\Facades\Cache::forget($lockKey);
}
}
}

class RateLimited
{
protected string $key;
protected int $maxAttempts;
protected int $decayMinutes;

public function __construct(string $key, int $maxAttempts = 1, int $decayMinutes = 60)
{
$this->key = $key;
$this->maxAttempts = $maxAttempts;
$this->decayMinutes = $decayMinutes;
}

public function handle($command, Closure $next)
{
$key = 'command:rate:' . $this->key;

$attempts = \Illuminate\Support\Facades\Cache::get($key, 0);

if ($attempts >= $this->maxAttempts) {
$command->warn('Rate limit exceeded');
return;
}

\Illuminate\Support\Facades\Cache::put($key, $attempts + 1, now()->addMinutes($this->decayMinutes));

return $next($command);
}
}

命令生成器

命令模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App\Console\Commands;

use Illuminate\Console\GeneratorCommand;

class MakeServiceCommand extends GeneratorCommand
{
protected $name = 'make:service';
protected $description = 'Create a new service class';
protected $type = 'Service';

protected function getStub(): string
{
return __DIR__ . '/stubs/service.stub';
}

protected function getDefaultNamespace($rootNamespace): string
{
return $rootNamespace . '\\Services';
}
}

总结

Laravel 13 的命令行系统提供了强大的 CLI 开发能力。通过批处理、交互式命令、中间件和调度机制,可以构建功能丰富的命令行工具。