返回

Laravel 11 定时任务:告别 Kernel.php, 用 routes/console.php 调度 Artisan 命令

php

Laravel 11 中如何调度 Artisan 命令?告别 Kernel.php

刚升级到 Laravel 11,或者刚开始用它?你可能最先注意到一个变化:以前那个熟悉的 app/Console/Kernel.php 文件不见了。这文件以前可是我们注册 Artisan 命令和定义定时任务的老地方。

那么问题来了,你创建了一个自定义命令,比如:

php artisan make:command expiration

这个命令可能是用来处理用户状态更新的,像下面这样:

<?php

namespace App\Console\Commands;

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

class expiration extends Command
{
    /**
     * 命令的名称及签名 (用于 Artisan 调用)
     */
    protected $signature = 'user:expiration'; // 最好遵循小写加冒号的约定, 比如 app:user-expiration

    /**
     * 命令的
     */
    protected $description = '每 5 分钟自动检查并更新用户状态'; // 清晰些

    /**
     * 执行命令的逻辑
     */
    public function handle()
    {
        // 使用 Eloquent 更新数据,更安全高效
        $updatedCount = User::where('expiration', 0)
                            ->update(['expiration' => 1]); // update 方法接受关联数组

        if ($updatedCount > 0) {
            $this->info("成功更新了 {$updatedCount} 个用户的状态。");
        } else {
            $this->info('没有需要更新状态的用户。');
        }
        
       // 避免直接在这里输出 'successful' 等硬编码字符串,使用 $this->info(), $this->comment(), $this->error() 等方法更专业
       // this->info('Command executed successfully.'); 
    }
}

写好了命令,你希望它能像以前一样,比如每 5 分钟自动执行一次。但在 Laravel 11 里,没有了 Kernel.php,该怎么配置呢?

别担心,Laravel 11 只是换了个地方来做这件事,而且 arguably,变得更简洁、更专注于任务调度本身了。

为啥 Kernel.php 不见了?

简单说,Laravel 团队想让项目结构更清晰。Kernel.php 过去承担了多个职责:控制台命令的注册、命令调度的定义,有时甚至还包括控制台相关的中间件。

在 Laravel 11 中:

  • HTTP Kernel (app/Http/Kernel.php) 仍然负责处理 Web 请求的中间件栈。
  • 控制台相关的逻辑 ,包括命令的注册和任务调度,现在统一放到了 routes/console.php 文件中。

这种分离使得 routes/console.php 成了处理所有命令行交互和自动化任务的核心位置,代码组织更一目了然。

在 Laravel 11 中安排定时任务

核心思路是利用 Illuminate\Support\Facades\Schedule Facade,在 routes/console.php 文件里定义你的调度任务。

1. 定位 routes/console.php

打开你的 Laravel 11 项目,找到 routes/console.php 文件。你会看到它默认可能包含一些示例代码或注释。这里就是我们配置定时任务的新家。

2. 定义你的调度任务

要让你的 user:expiration 命令(假设其签名为 user:expiration)每五分钟运行一次,你需要在 routes/console.php 文件里添加以下代码:

<?php

use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule; // 确保引入 Schedule Facade

/*
|--------------------------------------------------------------------------
| Console Routes
|--------------------------------------------------------------------------
|
| This file is where you may define all of your Closure based console
| commands. Each Closure is bound to a command instance allowing a
| simple approach to interacting with each command's IO methods.
|
*/

Artisan::command('inspire', function () {
    $this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote'); // 这是 Laravel 自带的示例


// 在这里添加你的定时任务调度
Schedule::command('user:expiration')->everyFiveMinutes();

// 你可以继续添加其他需要调度的任务
// Schedule::command('another:command')->daily();
// Schedule::command('backup:run')->hourly();

代码解释:

  • use Illuminate\Support\Facades\Schedule;: 首先,确保你导入了 Schedule Facade。
  • Schedule::command('user:expiration'): 这行代码告诉调度器,我们要调度的是哪个 Artisan 命令。这里的 'user:expiration' 必须和你命令类中定义的 $signature 属性完全一致。
  • ->everyFiveMinutes(): 这个链式方法指定了任务的执行频率——每五分钟一次。Laravel 提供了非常丰富的频率选项。

3. 常见的调度频率选项

除了 everyFiveMinutes(),还有很多开箱即用的频率设置方法,让你的调度更灵活:

  • ->hourly(): 每小时执行一次 (在 0 分时)。
  • ->daily(): 每天执行一次 (在 00:00)。
  • ->dailyAt('13:00'): 每天下午 1 点执行。
  • ->twiceDaily(1, 13): 每天执行两次 (在 1:00 和 13:00)。
  • ->weekly(): 每周执行一次 (周日 00:00)。
  • ->monthly(): 每月执行一次 (月初第一天 00:00)。
  • ->quarterly(): 每季度执行一次 (季度初第一天 00:00)。
  • ->yearly(): 每年执行一次 (年初第一天 00:00)。
  • ->everyMinute(): 每分钟执行一次。
  • ->everyTenMinutes(): 每十分钟执行一次。
  • ->everyThirtyMinutes(): 每三十分钟执行一次。
  • ->cron('* * * * *'): 对于更复杂的调度需求,可以直接使用 Cron 表达式。

你可以查阅 Laravel 官方文档 Task Scheduling 获取完整的频率选项列表和更高级的用法。

4. 确保调度器运行

仅仅在 routes/console.php 文件里定义了任务是不够的。你还需要告诉服务器,要定期去检查 Laravel 应用中是否有到期的任务需要执行。这通常通过配置系统的 Cron 服务来实现。

你需要向你的服务器的 Crontab 中添加一条记录。打开终端,输入 crontab -e,然后添加类似下面的一行:

* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

重要说明:

  • /path-to-your-project : 务必 将其替换成你 Laravel 项目的绝对路径
  • * * * * *: 这表示每分钟执行一次后面的命令。Laravel 调度器会自己判断哪些任务在当前这一分钟需要执行。
  • php artisan schedule:run: 这个 Artisan 命令是 Laravel 调度器的“心脏”,它负责检查所有在 routes/console.php 中定义的任务,并运行那些到期的任务。
  • >> /dev/null 2>&1: 这部分的意思是,将命令的标准输出 (stdout) 和错误输出 (stderr) 都重定向到 /dev/null 这个“黑洞”文件。这样做是为了防止 Cron 任务产生不必要的邮件通知。如果你在调试阶段,可以暂时去掉这部分,或者将输出重定向到指定日志文件,方便查看执行情况,例如:>> /path-to-your-project/storage/logs/cron.log 2>&1

安全建议:

  • 运行这条 Cron 任务的用户应该是你的 Web 服务器运行的用户(例如 www-data, nginx, apache)。这样可以避免因文件权限问题导致的任务失败。你可以使用 sudo -u www-data crontab -e (将 www-data 替换为实际用户) 来为特定用户编辑 Crontab。

5. 验证调度设置

配置完成后,有几种方式可以验证你的设置:

  • 查看已定义的任务列表:
    在项目根目录下运行:

    php artisan schedule:list
    

    这个命令会列出所有通过 routes/console.php 注册的定时任务、它们的执行频率以及下次预计执行时间。你应该能看到你的 user:expiration 命令。

  • 手动触发调度器(用于测试):
    如果你不想等 Cron 真正触发,可以手动运行调度检查命令:

    php artisan schedule:run
    

    这会立即执行所有当前时间点应该运行的任务。

  • 单独测试某个任务(推荐用于开发):
    有时你只想测试某个特定任务是否按预期工作,可以用:

    php artisan schedule:test user:expiration
    

    user:expiration 替换成你想测试的命令签名。这个命令会立即尝试运行指定的任务,并显示其输出或遇到的任何错误,非常适合开发调试。

进阶使用技巧

掌握了基础的调度方法后,还可以利用 Laravel 调度器提供的更多高级功能:

1. 任务输出管理

默认情况下,命令的输出会被丢弃(除非你在 Cron 条目中重定向)。你可以控制输出:

// 将输出发送到文件
Schedule::command('user:expiration')
          ->everyFiveMinutes()
          ->sendOutputTo('/path/to/your/logs/expiration.log');

// 追加输出到文件
Schedule::command('user:expiration')
          ->everyFiveMinutes()
          ->appendOutputTo('/path/to/your/logs/expiration.log');

// 将输出通过邮件发送
Schedule::command('emails:send')
          ->daily()
          ->emailOutputTo('your-email@example.com');

// 仅当命令失败时才发送邮件
Schedule::command('backup:database')
          ->daily()
          ->emailOutputOnFailure('admin-email@example.com');

2. 防止任务重叠

如果一个任务执行时间可能超过它的调度间隔(比如一个复杂的报表生成任务每 5 分钟执行一次,但有时需要 6 分钟才能完成),可能会导致同一个任务的多个实例同时运行。使用 withoutOverlapping() 可以避免这种情况:

Schedule::command('report:generate')
          ->everyTenMinutes()
          ->withoutOverlapping(); // 如果上一次任务仍在运行,则跳过本次调度

// 你还可以指定锁定的超时时间 (默认 24 小时)
Schedule::command('report:generate')
          ->everyTenMinutes()
          ->withoutOverlapping(15); // 如果任务运行超过 15 分钟,锁将释放

withoutOverlapping() 通常需要你的应用配置了支持原子锁的缓存驱动(如 Redis, Memcached, database 等)。

3. 只在一台服务器上运行任务 (负载均衡环境)

当你的应用部署在多台服务器上时,通常不希望定时任务在每台服务器上都执行一遍。可以使用 onOneServer() 方法,结合缓存系统,确保任务在整个集群中只执行一次:

Schedule::command('process:payments')
          ->hourly()
          ->onOneServer();

同样,这依赖于配置好的缓存驱动。

4. 维护模式下的行为

默认情况下,处于维护模式(通过 php artisan down 启动)的应用不会执行定时任务。如果你希望某个任务即使在维护模式下也要运行,可以使用 evenInMaintenanceMode()

Schedule::command('heartbeat:check')
          ->everyMinute()
          ->evenInMaintenanceMode();

5. 条件化调度

你可以根据环境或其他条件决定任务是否需要被调度:

// 仅在生产环境运行
Schedule::command('deploy:notify')
          ->daily()
          ->environments(['production']);

// 使用闭包进行更复杂的判断
Schedule::command('cleanup:temp')
          ->hourly()
          ->when(function () {
              // 比如,只在数据库连接正常时运行
              try {
                  DB::connection()->getPdo();
                  return true;
              } catch (\Exception $e) {
                  return false;
              }
          });

总结一下(不是总结陈词)

Laravel 11 中定时任务的配置方式虽然变了,从 app/Console/Kernel.php 移到了 routes/console.php,但核心理念没变,反而更加集中和清晰。通过 Schedule Facade 提供的丰富方法,你可以轻松定义各种频率和条件的任务调度。别忘了配置好服务器的 Cron 条目,让 php artisan schedule:run 定期执行,这是让整个调度系统动起来的关键一步。掌握了这些,你的 Laravel 应用就能像设定好的时钟一样,精确地执行自动化任务了。


相关资源(可选):