Laravel 13 任务调度器提供了强大的定时任务管理能力,本文深入探讨调度器的高级用法。

任务调度器架构

调度器配置

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
<?php

namespace App\Console;

use App\Console\Schedules\DailyTasks;
use App\Console\Schedules\HourlyTasks;
use App\Console\Schedules\WeeklyTasks;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule): void
{
$schedule->call(new DailyTasks)->daily();
$schedule->call(new HourlyTasks)->hourly();
$schedule->call(new WeeklyTasks)->weekly();

$schedule->command('cache:warm')
->hourly()
->withoutOverlapping()
->onOneServer();

$schedule->command('reports:generate')
->dailyAt('02:00')
->emailOutputTo('admin@example.com');
}
}

调度任务类

可调用任务

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
<?php

namespace App\Console\Schedules;

use App\Services\Report\ReportGenerator;
use Illuminate\Contracts\Scheduling\ShouldSchedule;

class DailyTasks implements ShouldSchedule
{
public function __construct(
protected ReportGenerator $reports
) {}

public function __invoke(): void
{
$this->cleanupExpiredTokens();
$this->generateDailyReports();
$this->sendDailyNotifications();
}

protected function cleanupExpiredTokens(): void
{
\App\Models\Token::where('expires_at', '<', now())->delete();
}

protected function generateDailyReports(): void
{
$this->reports->generateDaily();
}

protected function sendDailyNotifications(): void
{
\App\Jobs\SendDailyNotifications::dispatch();
}
}

条件任务

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
<?php

namespace App\Console\Schedules;

use App\Models\SystemSetting;
use Illuminate\Contracts\Scheduling\ShouldSchedule;

class ConditionalTasks implements ShouldSchedule
{
public function __invoke(): void
{
if ($this->shouldRunBackups()) {
$this->runBackup();
}

if ($this->shouldSendReports()) {
$this->sendReports();
}
}

protected function shouldRunBackups(): bool
{
return SystemSetting::get('backup_enabled', true);
}

protected function shouldSendReports(): bool
{
return !now()->isWeekend() && !now()->isHoliday();
}

protected function runBackup(): void
{
\Illuminate\Support\Facades\Artisan::call('backup:run');
}

protected function sendReports(): void
{
\App\Jobs\SendReports::dispatch();
}
}

调度器管理

调度器服务

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
<?php

namespace App\Services\Scheduler;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\Collection;

class SchedulerService
{
protected Schedule $schedule;
protected Collection $tasks;

public function __construct(Schedule $schedule)
{
$this->schedule = $schedule;
$this->tasks = collect();
}

public function register(ScheduledTask $task): self
{
$this->tasks->push($task);

$event = $this->schedule->command($task->command, $task->parameters ?? []);

$this->applyFrequency($event, $task);
$this->applyConstraints($event, $task);
$this->applyHooks($event, $task);

return $this;
}

protected function applyFrequency($event, ScheduledTask $task): void
{
match ($task->frequency) {
'every_minute' => $event->everyMinute(),
'every_five_minutes' => $event->everyFiveMinutes(),
'every_fifteen_minutes' => $event->everyFifteenMinutes(),
'hourly' => $event->hourly(),
'daily' => $event->daily(),
'weekly' => $event->weekly(),
'monthly' => $event->monthly(),
default => $event->cron($task->expression),
};
}

protected function applyConstraints($event, ScheduledTask $task): void
{
if ($task->timezone) {
$event->timezone($task->timezone);
}

if ($task->without_overlapping) {
$event->withoutOverlapping($task->overlap_minutes ?? 1440);
}

if ($task->on_one_server) {
$event->onOneServer();
}

if ($task->run_in_maintenance) {
$event->evenInMaintenanceMode();
}

if ($task->environments) {
$event->environments($task->environments);
}
}

protected function applyHooks($event, ScheduledTask $task): void
{
if ($task->before_callback) {
$event->before($task->before_callback);
}

if ($task->after_callback) {
$event->after($task->after_callback);
}

if ($task->on_success_callback) {
$event->onSuccess($task->on_success_callback);
}

if ($task->on_failure_callback) {
$event->onFailure($task->on_failure_callback);
}
}

public function getTasks(): Collection
{
return $this->tasks;
}
}

动态调度

数据库驱动的调度

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
<?php

namespace App\Services\Scheduler;

use App\Models\ScheduledTask;
use Illuminate\Console\Scheduling\Schedule;

class DynamicScheduler
{
protected Schedule $schedule;

public function __construct(Schedule $schedule)
{
$this->schedule = $schedule;
}

public function loadFromDatabase(): void
{
$tasks = ScheduledTask::where('is_active', true)->get();

foreach ($tasks as $task) {
$this->registerTask($task);
}
}

protected function registerTask(ScheduledTask $task): void
{
$event = $this->schedule->command($task->command, $task->parameters ?? []);

if ($task->expression) {
$event->cron($task->expression);
} else {
$this->applyFrequency($event, $task);
}

if ($task->without_overlapping) {
$event->withoutOverlapping($task->overlap_minutes ?? 1440);
}

if ($task->on_one_server) {
$event->onOneServer();
}

$event->onSuccess(function () use ($task) {
$task->update([
'last_run_at' => now(),
'last_run_status' => 'success',
]);
});

$event->onFailure(function () use ($task) {
$task->update([
'last_run_at' => now(),
'last_run_status' => 'failed',
]);
});
}

protected function applyFrequency($event, ScheduledTask $task): void
{
match ($task->frequency) {
'every_minute' => $event->everyMinute(),
'hourly' => $event->hourly(),
'daily' => $event->daily(),
'weekly' => $event->weekly(),
'monthly' => $event->monthly(),
default => $event->daily(),
};
}
}

调度器监控

任务执行记录

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
<?php

namespace App\Services\Scheduler;

use App\Models\TaskExecution;
use Illuminate\Console\Events\ScheduledTaskFinished;
use Illuminate\Console\Events\ScheduledTaskStarting;
use Illuminate\Events\Dispatcher;

class SchedulerMonitor
{
protected array $executions = [];

public function subscribe(Dispatcher $events): void
{
$events->listen(ScheduledTaskStarting::class, [$this, 'taskStarting']);
$events->listen(ScheduledTaskFinished::class, [$this, 'taskFinished']);
}

public function taskStarting(ScheduledTaskStarting $event): void
{
$execution = TaskExecution::create([
'task_name' => $this->getTaskName($event),
'command' => $event->task->command ?? $event->task->description,
'started_at' => now(),
'status' => 'running',
]);

$this->executions[$this->getTaskKey($event)] = $execution;
}

public function taskFinished(ScheduledTaskFinished $event): void
{
$key = $this->getTaskKey($event);

if (!isset($this->executions[$key])) {
return;
}

$execution = $this->executions[$key];

$execution->update([
'finished_at' => now(),
'duration' => $execution->started_at->diffInSeconds(now()),
'status' => $event->task->exitCode === 0 ? 'success' : 'failed',
'exit_code' => $event->task->exitCode,
'output' => $event->task->output ?? null,
]);

unset($this->executions[$key]);
}

protected function getTaskName($event): string
{
return $event->task->description ?? $event->task->command ?? 'unknown';
}

protected function getTaskKey($event): string
{
return spl_object_hash($event->task);
}
}

调度器命令

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
<?php

namespace App\Console\Commands;

use App\Models\ScheduledTask;
use Illuminate\Console\Command;

class SchedulerCommand extends Command
{
protected $signature = 'scheduler:manage {action} {task?}';
protected $description = 'Manage scheduled tasks';

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

return match ($action) {
'list' => $this->listTasks(),
'enable' => $this->enableTask(),
'disable' => $this->disableTask(),
'run' => $this->runTask(),
default => $this->invalidAction(),
};
}

protected function listTasks(): int
{
$tasks = ScheduledTask::all();

$this->table(
['ID', 'Command', 'Frequency', 'Active', 'Last Run'],
$tasks->map(fn($t) => [
$t->id,
$t->command,
$t->frequency ?? $t->expression,
$t->is_active ? 'Yes' : 'No',
$t->last_run_at?->diffForHumans() ?? 'Never',
])
);

return self::SUCCESS;
}

protected function enableTask(): int
{
$taskId = $this->argument('task');

if (!$taskId) {
$this->error('Please specify a task ID');
return self::FAILURE;
}

ScheduledTask::where('id', $taskId)->update(['is_active' => true]);

$this->info("Task {$taskId} enabled");

return self::SUCCESS;
}

protected function disableTask(): int
{
$taskId = $this->argument('task');

if (!$taskId) {
$this->error('Please specify a task ID');
return self::FAILURE;
}

ScheduledTask::where('id', $taskId)->update(['is_active' => false]);

$this->info("Task {$taskId} disabled");

return self::SUCCESS;
}

protected function runTask(): int
{
$taskId = $this->argument('task');

if (!$taskId) {
$this->error('Please specify a task ID');
return self::FAILURE;
}

$task = ScheduledTask::find($taskId);

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

$this->info("Running task: {$task->command}");

\Illuminate\Support\Facades\Artisan::call($task->command, $task->parameters ?? []);

$this->line(\Illuminate\Support\Facades\Artisan::output());

return self::SUCCESS;
}

protected function invalidAction(): int
{
$this->error('Invalid action. Use: list, enable, disable, or run');
return self::FAILURE;
}
}

总结

Laravel 13 任务调度器提供了灵活的定时任务管理能力。通过可调用任务、动态调度和监控机制,可以构建可靠的定时任务系统。