简介 过去,你可能需要在服务器上为每一个调度任务去创建 Cron 条目。但是,这种方式很快会变得很痛苦,因为这些任务调度不在源代码中,并且你每次都需要通过 SSH 链接登录到服务器中才能增加 Cron 条目。 Laravel 的命令行调度器允许你在 Laravel 中清晰明了地定义命令调度。在使用这个任务调度器时,你只需要在你的服务器上创建单个 Cron 入口。你的任务调度在 app/Console/Kernel.php 的 schedule 方法中进行定义。为了帮助你更好的入门,这个方法中有个简单的例子。 启动调度器 使用这个调度器时,只需要把下面的 Cron 条目添加到你的服务器中。如果你不知道如何在服务器中添加 Cron 条目,可以考虑使用诸如 Laravel Forge 之类的服务来管理 Cron 条目: ``` * * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1 ``` 该 Cron 会每分钟调用一次 Laravel 的命令行调度器。在执行 schedule:run 命令时,Laravel 会根据你的调度执行预定的程序。 定义调度 你可以在 App\Console\Kernel 类的 schedule 方法中定义所有的调度任务。 在开始之前,让我们来看一个例子。在此例中,我们计划每天午夜执行一个 闭包。在 闭包 中,我们会执行一个数据库查询来清空一张表: ``` <?php namespace App\Console; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Support\Facades\DB; class Kernel extends ConsoleKernel { /** * 应用中自定义的 Artisan 命令 * * @var array */ protected $commands = [ // ]; /** * 定义应用中的命令调度 * * @param \Illuminate\Console\Scheduling\Schedule $schedule * @return void */ protected function schedule(Schedule $schedule) { $schedule->call(function () { DB::table('recent_users')->delete(); })->daily(); } } ``` 除了使用闭包来定义任务调度外,你也可以使用 可调用对象。可调用对象是简单的 PHP 类,包含一个 __invoke 方法: ``` $schedule->call(new DeleteRecentUsers)->daily(); ``` Artisan 命令调度 除了调用闭包这种方式来调度外,你还可以调用 Artisan commands 和操作系统命令。例如,你可以给 command 方法传递命令名称或者类来调度一个 Artisan 命令: ``` $schedule->command('emails:send Taylor --force')->daily(); $schedule->command(EmailsCommand::class, ['Taylor', '--force'])->daily(); ``` 队列任务调度 job 方法可以用来调度 队列任务 。此方法提供了一种快捷方式来调度任务,而无需使用 call 方法创建闭包来调度任务: ``` $schedule->job(new Heartbeat)->everyFiveMinutes(); // 分发任务到「heartbeats」队列... $schedule->job(new Heartbeat, 'heartbeats')->everyFiveMinutes(); ``` Shell 调度命令 exec 方法可用于向操作系统发送命令: ``` $schedule->exec('node /home/forge/script.js')->daily(); ``` 调度频率选项 你可以为你的任务分配多种调度计划: |方法 |描述| |----|----| |->cron('* * * * *'); |自定义 Cron 计划执行任务| |->everyMinute();| 每分钟执行一次任务| |->everyTwoMinutes(); |每两分钟执行一次任务| |->everyThreeMinutes();| 每三分钟执行一次任务| |->everyFourMinutes(); |每四分钟执行一次任务| |->everyFiveMinutes(); |每五分钟执行一次任务| |->everyTenMinutes(); |每十分钟执行一次任务| |->everyFifteenMinutes(); |每十五分钟执行一次任务| |->everyThirtyMinutes(); |每三十分钟执行一次任务| |->hourly(); |每小时执行一次任务| |->hourlyAt(17); |每小时第 17 分钟执行一次任务| |->everyTwoHours();| 每两小时执行一次任务| |->everyThreeHours(); |每三小时执行一次任务| |->everyFourHours(); |每四小时执行一次任务| |->everySixHours(); |每六小时执行一次任务| |->daily(); |每天 0 点执行一次任务| |->dailyAt('13:00'); |每天 13:00 执行一次任务| |->twiceDaily(1, 13); |每天 01:00 和 13:00 各执行一次任务| |->weekly(); |每周日 00:00 执行一次任务| |->weeklyOn(1, '8:00'); |每周一的 08:00 执行一次任务| |->monthly(); |每月第一天 00:00 执行一次任务| |->monthlyOn(4, '15:00'); |每月 4 号的 15:00 执行一次任务| |->lastDayOfMonth('15:00'); |每月最后一天 15:00 执行一次任务| |->quarterly(); |每季度第一天 00:00 执行一次任务| |->yearly(); |每年第一天 00:00 执行一次任务| |->timezone('America/New_York'); |设置时区| 这些方法可以与额外的约束条件相结合,此时我们便可创建在一周的特定时间运行的甚至是更精细的计划任务。例如,在每周一执行命令: // 在每周一的 13:00 执行... ``` $schedule->call(function () { // })->weekly()->mondays()->at('13:00'); ``` // 在每个工作日的 8:00 到 17:00 之间的每个小时周期执行... ``` $schedule->command('foo') ->weekdays() ->hourly() ->timezone('America/Chicago') ->between('8:00', '17:00'); ``` 下方列出了额外的约束条件: |方法 |描述| |----|----| |->weekdays(); |限制任务在工作日执行| |->weekends(); |限制任务在周末执行| |->sundays(); |限制任务在周日执行| |->mondays(); |限制任务在周一执行| |->tuesdays(); |限制任务在周二执行| |->wednesdays();| 限制任务在周三执行| |->thursdays(); |限制任务在周四执行| |->fridays(); |限制任务在周五执行| |->saturdays(); |限制任务在周六执行| |->days(arrayΙmixed); |限制任务在每周的指定日期执行| |->between($start, $end); |限制任务在 $start 和 $end 区间执行| |->when(Closure); |限制任务在闭包返回为真时执行| |->environments($env); |限制任务在特定环境中执行| 周几(Day)限制 days 方法可以用于限制任务在每周的哪一天执行。举个例子,您可以在让一个命令每周的周日和周三每小时执行一次: ``` $schedule->command('reminders:send') ->hourly() ->days([0, 3]); ``` 时间范围限制 between 方法可用于限制任务在一天中的某个时间段执行: ``` $schedule->command('reminders:send') ->hourly() ->between('7:00', '22:00'); ``` 相似地, unlessBetween 方法也可用于限制任务不在一天中的某个时间段执行: ``` $schedule->command('reminders:send') ->hourly() ->unlessBetween('23:00', '4:00'); ``` 闭包检测限制 when 方法可根据检测结果来执行任务。换言之,若给定的闭包返回 true ,若无其他限制条件阻止,任务就会一直执行: ``` $schedule->command('emails:send')->daily()->when(function () { return true; }); ``` skip 可看作是 when 的逆方法。若 skip 方法返回 true ,任务将不会执行 ``` $schedule->command('emails:send')->daily()->skip(function () { return true; }); ``` 当链式调用 when 方法时,仅当所有 when 都返回 true 时,任务才会执行。 环境限制 environments 方法可限制任务在给定环境中执行: ``` $schedule->command('emails:send') ->daily() ->environments(['staging', 'production']); ``` 时区 timezone 方法可指定任务在给定时区中的时间执行: ``` $schedule->command('report:generate') ->timezone('America/New_York') ->at('02:00') ``` 若想给所有计划任务分配相同的时区,那么需要在 app/Console/Kernel.php 文件中定义 scheduleTimezone 方法。这个方法会返回一个默认时区,最终分配给所有计划任务: ``` /** * 获取计划事件默认使用的时区 * * @return \DateTimeZone|string|null */ protected function scheduleTimezone() { return 'America/Chicago'; } ``` 注意:请记住,有些时区会使用夏令时。当夏令时发生调整时,你的任务可能会执行两次,甚至根本不会执行。因此,我们建议尽可能避免使用时区来安排计划任务。 避免任务重复 默认情况下,即使之前的任务实例还在执行,调度内的任务也会执行。为避免这种情况的发生,你可以使用 withoutOverlapping 方法: ``` $schedule->command('emails:send')->withoutOverlapping(); ``` 在此例中,若 emails:send Artisan 命令 还未运行,那它将会每分钟执行一次。当你的任务执行时间非常不确定,导致你无法准确预测任务的执行时间,那 withoutOverlapping 方法会特别有用。 如有需要,你可以在「without overlapping」锁过期之前,指定它的过期时间。默认情况下,这个锁会在 24 小时后过期。 ``` $schedule->command('emails:send')->withoutOverlapping(10); ``` 任务只运行在一台服务器上 注意:你的应用默认缓存驱动必须是 database 、 memcached 或 redis 才能使用这个特性。除此之外,所有服务器必须使用同一台中央缓存服务器来通信。 若你的应用在多台服务器中运行,你可能需要限制某个计划任务仅在单台服务器上运行。假如你有一个计划任务:在每周五晚生成一份新报告。若此任务调度器在三台服务器上运行,那么该任务就会在三台服务器上运行并生成三份报告。这样不好。 当你定义计划任务时,可使用 onOneServer 方法表明该任务仅在单台服务器上运行。如此,第一台获取到该任务的服务器,会给它附加一个原子锁,这样便可防止其他服务器在同一时间内执行同一个任务: ``` $schedule->command('report:generate') ->fridays() ->at('17:00') ->onOneServer(); ``` 后台任务 默认情况下,计划同时运行的多个命令将会顺序执行。若你有长时间运行的命令,这可能导致后续命令的启动时间比预期的更晚。你可以使用 runInBackground 方法让命令在后台运行,如此,它们便可同时运行了: ``` $schedule->command('analytics:report') ->daily() ->runInBackground(); ``` 注意:仅能在通过 command 和 exec 方法调度任务时,方能使用 runInBackground 方法。 维护模式 Laravel 的队列任务在 维护模式 下不会运行。因为我们不想你的调度任务干扰到你服务器上可能还未完成的项目。不过,如果你确实是想在维护模式下强制调度任务执行,你可以使用 evenInMaintenanceMode 方法: ``` $schedule->command('emails:send')->evenInMaintenanceMode(); ``` 任务输出 Laravel 调度器提供了一些方便的方法来处理调度任务输出。首先,你可以使用 sendOutputTo 方法来输出到文件以便于后续检查: ``` $schedule->command('emails:send') ->daily() ->sendOutputTo($filePath); ``` 如果希望将输出 附加 到给定文件,可以使用 appendOutputTo 方法: ``` $schedule->command('emails:send') ->daily() ->appendOutputTo($filePath); ``` 使用 emailOutputTo 方法,你可以将输出发送到指定邮箱。在使用邮件发送之前,你需要配置 Laravel 的 邮件服务: ``` $schedule->command('foo') ->daily() ->sendOutputTo($filePath) ->emailOutputTo('foo@example.com'); ``` 如果你只想在命令执行失败的时候输出到邮箱,你可以使用 emailOutputOnFailure 方法: ``` $schedule->command('foo') ->daily() ->emailOutputOnFailure('foo@example.com'); ``` 注意:emailOutputTo,emailOutputOnFailure,sendOutputTo 和 appendOutputTo 方法是 command 和 exec 独有的。 任务钩子 使用 before 和 after 方法,你可以指定在调度任务执行前或者执行后来运行的特定代码: ``` $schedule->command('emails:send') ->daily() ->before(function () { // 任务即将开始... }) ->after(function () { // 任务完成... }); ``` onSuccess 和 onFailure 方法允许您指定计划任务成功或失败要执行的代码: ``` $schedule->command('emails:send') ->daily() ->onSuccess(function () { // 任务成功... }) ->onFailure(function () { // 任务失败... }); ``` 如果你的命令提供了有效输出,你可以在闭包中将 Illuminate\Support\Stringable 实例化为 $output 在 after , onSuccess 或 onFailure 中对其进行访问: ``` use Illuminate\Support\Stringable; $schedule->command('emails:send') ->daily() ->onSuccess(function (Stringable $output) { // 任务成功... }) ->onFailure(function (Stringable $output) { // 任务失败... }); ``` Pinging 网址 使用 pingBefore 和 thenPing 方法,你可以在任务完成之前或完成之后来 ping 指定的 URL。当前方法在通知外部服务 [如 Laravel Envoyer] ( envoyer.io ) 计划任务在将要执行或已完成时会很有用: ``` $schedule->command('emails:send') ->daily() ->pingBefore($url) ->thenPing($url); ``` 只有当条件为 true 时,才可以使用 pingBeforeIf 和 thenPingIf 方法来 ping 给定 URL : ``` $schedule->command('emails:send') ->daily() ->pingBeforeIf($condition, $url) ->thenPingIf($condition, $url); ``` 当任务成功或失败时,使用 pingOnSuccess 和 pingOnFailure 方法来 ping 给定 URL: ``` $schedule->command('emails:send') ->daily() ->pingOnSuccess($successUrl) ->pingOnFailure($failureUrl); ``` 所有 ping 方法都需要 Guzzle HTTP 库。你可以使用 composer 将 Guzzle 添加到你的项目中: ``` composer require guzzlehttp/guzzle ```