前言
Laravel 中可以很方便地定義任務排程,在 app/Console/Kernel.php
的 schedule
方法中定義 call
、command
、job
以及exec
的執行間隔即可。
在實際開發過程中,我們發現如果需要修改任務排程的執行時間間隔,或者關閉某個任務排程,都需要重新修改程式碼提交,重新構建釋出,體驗不是很好。
這裡分享一個基於資料表的配置來管理 Laravel 應用程式中任務排程的方案,可以一起參與討論一下。
實現過程
在討論實現之前,先梳理一下需要優化的點,並整理一下實現思路。
需求
- 能夠靈活地配置任務排程的執行間隔
- 允許開啟關閉任務的排程
- 適配 laravel 的任務排程引數,保持風格統一
- 簡單地封裝擴充套件,不增加負擔
思路
可以在 Schedule 例項化以後通過讀取 schedules 資料表的配置來定義執行任務排程,可以在此基礎上進行簡單封裝讓多個專案中也可以使用。
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedules = ScheduleModel::active()->get();
foreach($schedules as $schedule){
$schedule->command($schedule->command .'' .$schedule->parameters)->cron($schedule->expression);
}
// $schedule->command('inspire')->hourly();
}
實現
Schedule 通過服務容器 singleton
例項化後依賴注入,可以通過容器的 resolving
方法繫結一個回撥函式在 Schedule 例項化後執行,在回撥函式中加入讀取 schedules 配置的邏輯。
// vendor/jiannei/laravel-schedule/src/Providers/LaravelServiceProvider.php
$this->app->resolving(Schedule::class, function ($schedule) {
$this->schedule($schedule);
});
protected function schedule(Schedule $schedule): void
{
try {
$schedules = app(Config::get('schedule.model'))->active()->get();
} catch (QueryException $exception) {
$schedules = collect();
}
$schedules->each(function ($item) use ($schedule) {
$event = $schedule->command($item->command.' '.$item->parameters);
$event->cron($item->expression)
->name($item->description)
->timezone($item->timezone);
if (class_exists($enum = Config::get('schedule.enum'))) {
$scheduleEnum = $enum::fromValue($item->command);
$callbacks = ['skip', 'when', 'before', 'after', 'onSuccess', 'onFailure'];
foreach ($callbacks as $callback) {
if ($method = $scheduleEnum->hasCallback($callback)) {
$event->$callback($scheduleEnum->$method($event, $item));
}
}
}
if ($item->environments) {
$event->environments($item->environments);
}
if ($item->without_overlapping) {
$event->withoutOverlapping($item->without_overlapping);
}
if ($item->on_one_server) {
$event->onOneServer();
}
if ($item->in_background) {
$event->runInBackground();
}
if ($item->in_maintenance_mode) {
$event->evenInMaintenanceMode();
}
if ($item->output_file_path) {
if ($item->output_append) {
$event->appendOutputTo(Config::get('schedule.output.path').Str::start($item->output_file_path, DIRECTORY_SEPARATOR));
} else {
$event->sendOutputTo(Config::get('schedule.output.path').Str::start($item->output_file_path, DIRECTORY_SEPARATOR));
}
}
if ($item->output_email) {
if ($item->output_email_on_failure) {
$event->emailOutputOnFailure($item->output_email);
} else {
$event->emailOutputTo($item->output_email);
}
}
});
}
安裝和使用
Package 已釋出,可以檢視相應的文件
原理
在實現前面的需求後,一起討論下 Laravel 應用中通過 php artisan schedule:run
能夠進行任務排程的原理。
在 Laravel 專案中部署任務排程,通常的 Linux crontab 配置如下:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
這裡涉及到使用 Linux 的 crontab 每分鐘通過 php-cli 間隔執行 Laravel 的 artisan 檔案
php artisan schedule:run
說明:
- php cli 模式下每分鐘間隔執行 Laravel 的 artisan 檔案
- artisan 是 Laravel 命令列執行模式的入口檔案
- 通過 artisan 入口檔案,解析後面的
schedule:run
引數,最終執行vendor/laravel/framework/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php
中的handle
方法
1. php artisan 的執行
bootstrap/app.php
// 註冊 Illuminate\Contracts\Console\Kernel::class和App\Console\Kernel::class 的繫結關係
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
artisan
// 根據上一步的繫結關係,例項化 App\Console\Kernel
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
// 執行 App\Console\Kernel 的 handle 方法
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput,
new Symfony\Component\Console\Output\ConsoleOutput
);
// 執行 App\Console\Kernel 的 terminate 方法
$kernel->terminate($input, $status);
exit($status);
app/Console/Kernel.php
中繼承了Illuminate\Foundation\Console\Kernel
的handle
和terminate
方法vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php
我們需要關心__construct
、handle
、terminate
public function __construct(Application $app, Dispatcher $events)
{
if (! defined('ARTISAN_BINARY')) {
define('ARTISAN_BINARY', 'artisan');
}
$this->app = $app;
$this->events = $events;
// 在應用服務啟動後執行控制檯執任務
$this->app->booted(function () {
$this->defineConsoleSchedule();
});
}
public function handle($input, $output = null)
{
try {
$this->bootstrap();
return $this->getArtisan()->run($input, $output);
} catch (Throwable $e) {
$this->reportException($e);
$this->renderException($output, $e);
return 1;
}
}
public function terminate($input, $status)
{
$this->app->terminate();
}
protected function defineConsoleSchedule()
{
// Illuminate\Console\Scheduling\Schedule::class 例項化時呼叫 schedule 方法執行任務排程
$this->app->singleton(Schedule::class, function ($app) {
return tap(new Schedule($this->scheduleTimezone()), function ($schedule) {
$this->schedule($schedule->useCache($this->scheduleCache()));
});
});
}
app/Console/Kernel.php
中覆蓋了Illuminate\Foundation\Console\Kernel
的schedule
方法,也就是以前經常定義任務排程執行的地方
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')->hourly();
}
從上面的分析可以看出,php artisan
執行會註冊Illuminate\Console\Scheduling\Schedule::class
,等Illuminate\Console\Scheduling\Schedule::class
例項化時執行定義在 app/Console/Kernel.php
的 schedule
方法中定義的任務排程。
補充:
php artisan
等價於php artisan list
,- 分析
vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php
中的getArtisan
方法可以瞭解如何將 artisan 後面的 list 引數解析成需要執行的 command
2. php artisan schedule:run 的執行
artisan 解析
schedule:run
引數,執行vendor/laravel/framework/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php
中的handle
方法handle
方法中注入\Illuminate\Console\Scheduling\Schedule
例項
// vendor/laravel/framework/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php
public function handle(Schedule $schedule, Dispatcher $dispatcher, ExceptionHandler $handler)
{
$this->schedule = $schedule;
$this->dispatcher = $dispatcher;
$this->handler = $handler;
foreach ($this->schedule->dueEvents($this->laravel) as $event) {
if (! $event->filtersPass($this->laravel)) {
$this->dispatcher->dispatch(new ScheduledTaskSkipped($event));
continue;
}
if ($event->onOneServer) {
$this->runSingleServerEvent($event);
} else {
$this->runEvent($event);
}
$this->eventsRan = true;
}
if (! $this->eventsRan) {
$this->info('No scheduled commands are ready to run.');
}
}
- 結合前面
php artisan
的分析,在\Illuminate\Console\Scheduling\Schedule
例項化時便會呼叫app/Console/Kernel.php
中的schedule
方法中定義的任務排程
其他
如果對您的日常工作有所幫助或啟發,歡迎 star + fork + follow
。
如果有任何批評建議,通過郵箱(longjian.huang@foxmail.com)的方式可以聯絡到我。
總之,歡迎各路英雄好漢。
QQ 群:1105120693
本作品採用《CC 協議》,轉載必須註明作者和本文連結