共计 5005 个字符,预计需要花费 13 分钟才能阅读完成。
Console 内核
上一篇文章我们介绍了 Laravel 的 HTTP 内核,详细概述了网络请求从进入应用到应用处理完请求返回 HTTP 响应整个生命周期中 HTTP 内核是如何调动 Laravel 各个核心组件来完成任务的。除了处理 HTTP 请求一个健壮的应用经常还会需要执行计划任务、异步队列这些。Laravel 为了能让应用满足这些场景设计了 artisan 工具,通过 artisan 工具定义各种命令来满足非 HTTP 请求的各种场景,artisan 命令通过 Laravel 的 Console 内核来完成对应用核心组件的调度来完成任务。今天我们就来学习一下 Laravel Console 内核的核心代码。
内核绑定
跟 HTTP 内核一样,在应用初始化阶有一个内核绑定的过程,将 Console 内核注册到应用的服务容器里去,还是引用上一篇文章引用过的 bootstrap/app.php 里的代码
<?php
// 第一部分:创建应用实例
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.’/../’)
);
// 第二部分:完成内核绑定
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
// console 内核绑定
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
return $app;
Console 内核 \App\Console\Kernel 继承自 Illuminate\Foundation\Console,在 Console 内核中我们可以注册 artisan 命令和定义应用里要执行的计划任务。
/**
* Define the application’s command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command(‘inspire’)
// ->hourly();
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__.’/Commands’);
require base_path(‘routes/console.php’);
}
在实例化 Console 内核的时候,内核会定义应用的命令计划任务 (shedule 方法中定义的计划任务)
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();
});
}
应用解析 Console 内核
查看 aritisan 文件的源码我们可以看到,完成 Console 内核绑定的绑定后,接下来就会通过服务容器解析出 console 内核对象
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput,
new Symfony\Component\Console\Output\ConsoleOutput
);
执行命令任务
解析出 Console 内核对象后,接下来就要处理来自命令行的命令请求了,我们都知道 PHP 是通过全局变量 $_SERVER[‘argv’] 来接收所有的命令行输入的,和命令行里执行 shell 脚本一样(在 shell 脚本里可以通过 $0 获取脚本文件名,$1 $2 这些依次获取后面传递给 shell 脚本的参数选项)索引 0 对应的是脚本文件名,接下来依次是命令行里传递给脚本的所有参数选项,所以在命令行里通过 artisan 脚本执行的命令,在 artisan 脚本中 $_SERVER[‘argv’] 数组里索引 0 对应的永远是 artisan 这个字符串,命令行里后面的参数会依次对应到 $_SERVER[‘argv’] 数组后续的元素里。
因为 artisan 命令的语法中可以指定命令参数选项、有的选项还可以指定实参,为了减少命令行输入参数解析的复杂度,Laravel 使用了 Symfony\Component\Console\Input 对象来解析命令行里这些参数选项(shell 脚本里其实也是一样,会通过 shell 函数 getopts 来解析各种格式的命令行参数输入),同样地 Laravel 使用了 Symfony\Component\Console\Output 对象来抽象化命令行的标准输出。
引导应用
在 Console 内核的 handle 方法里我们可以看到和 HTTP 内核处理请求前使用 bootstrapper 程序引用应用一样在开始处理命令任务之前也会有引导应用这一步操作
其父类「IlluminateFoundationConsoleKernel」内部定义了属性名为「bootstrappers」的 引导程序 数组:
protected $bootstrappers = [
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
\Illuminate\Foundation\Bootstrap\SetRequestForConsole::class,
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
数组中包括的引导程序基本上和 HTTP 内核中定义的引导程序一样,都是应用在初始化阶段要进行的环境变量、配置文件加载、注册异常处理器、设置 Console 请求、注册应用中的服务容器、Facade 和启动服务。其中设置 Console 请求是唯一区别于 HTTP 内核的一个引导程序。
执行命令
执行命令是通过 Console Application 来执行的,它继承自 Symfony 框架的 Symfony\Component\Console\Application 类,通过对应的 run 方法来执行命令。
name Illuminate\Foundation\Console;
class Kernel implements KernelContract
{
public function handle($input, $output = null)
{
try {
$this->bootstrap();
return $this->getArtisan()->run($input, $output);
} catch (Exception $e) {
$this->reportException($e);
$this->renderException($output, $e);
return 1;
} catch (Throwable $e) {
$e = new FatalThrowableError($e);
$this->reportException($e);
$this->renderException($output, $e);
return 1;
}
}
}
namespace Symfony\Component\Console;
class Application
{
// 执行命令
public function run(InputInterface $input = null, OutputInterface $output = null)
{
……
try {
$exitCode = $this->doRun($input, $output);
} catch {
……
}
……
return $exitCode;
}
public function doRun(InputInterface $input, OutputInterface $output)
{
// 解析出命令名称
$name = $this->getCommandName($input);
// 解析出入参
if (!$name) {
$name = $this->defaultCommand;
$definition = $this->getDefinition();
$definition->setArguments(array_merge(
$definition->getArguments(),
array(
‘command’ => new InputArgument(‘command’, InputArgument::OPTIONAL, $definition->getArgument(‘command’)->getDescription(), $name),
)
));
}
……
try {
// 通过命令名称查找出命令类(命名空间、类名等)
$command = $this->find($name);
}
……
// 运行命令类
$exitCode = $this->doRunCommand($command, $input, $output);
return $exitCode;
}
protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
{
……
// 执行命令类的 run 方法来处理任务
$exitCode = $command->run($input, $output);
……
return $exitcode;
}
}
执行命令时主要有四步操作:
通过命令行输入解析出命令名称和参数选项。
通过命令名称查找命令类的命名空间和类名。
执行命令类的 run 方法来完成任务处理并返回状态码。
和命令行脚本的规范一样,如果执行命令任务程序成功会返回 0, 抛出异常退出则返回 1。
还有就是打开命令类后我们可以看到并没有 run 方法,我们把处理逻辑都写在了 handle 方法中,仔细查看代码会发现 run 方法定义在父类中,在 run 方法会中会调用子类中定义的 handle 方法来完成任务处理。严格遵循了面向对象程序设计的 SOLID 原则。
结束应用
执行完命令程序返回状态码后,在 artisan 中会直接通过 exit($status) 函数输出状态码并结束 PHP 进程,接下来 shell 进程会根据返回的状态码是否为 0 来判断脚本命令是否执行成功。
到这里通过命令行开启的程序进程到这里就结束了,跟 HTTP 内核一样 Console 内核在整个生命周期中也是负责调度,只不过 Http 内核最终将请求落地到了 Controller 程序中而 Console 内核则是将命令行请求落地到了 Laravel 中定义的各种命令类程序中,然后在命令类里面我们就可以写其他程序一样自由地使用 Laravel 中的各个组件和注册到服务容器里的服务了。
本文已经收录在系列文章 Laravel 源码学习里,欢迎访问阅读。