任务调度,在 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 $item){ | |
$schedule->command($item->command .'' .$item->parameters)->cron($item->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