ConsoleKernel在上一篇文章中,我们介绍了Laravel的HTTP内核,详细介绍了HTTP内核如何调动Laravel在一个网络请求从进入应用到返回HTTP的整个生命周期应用程序处理请求后的响应每个核心组件完成的任务。除了处理HTTP请求之外,一个健壮的应用程序通常还需要执行计划任务、异步队列等。Laravel设计了??artisan工具来让应用程序满足这些场景。artisan工具定义了各种命令来满足非HTTP请求的各种场景。artisan命令通过LaravelConsole核心完成对应用核心组件的调度,完成任务。今天我们就来了解一下LaravelConsole核心的核心代码。内核绑定与HTTP内核相同。在应用程序初始化阶段有一个内核绑定过程。Console内核注册在应用服务容器中,或者参考上一篇文章引用的bootstrap/app.php中的代码singleton(Illuminate\Contracts\Http\Kernel::class,App\Http\Kernel::class);//控制台内核绑定$app->singleton(Illuminate\Contracts\Console\Kernel::class,App\Console\Kernel::class);$app->singleton(Illuminate\Contracts\Debug\ExceptionHandler::class,App\Exceptions\Handler::class);return$app;ConsoleKernel\App\Console\Kernel继承自Illuminate\Foundation\Console,在Console内核中,我们可以注册artisan命令,定义应用中要执行的定时任务。/***定义应用程序的命令调度。**@param\Illuminate\Console\Scheduling\Schedule$schedule*@returnvoid*/protectedfunctionschedule(Schedule$schedule){//$schedule->command('inspire')//->hourly();}/***为应用程序注册命令。**@returnvoid*/protectedfunctioncommands(){$this->load(__DIR__.'/Commands');requirebase_path('routes/console.php');}在实例化Console内核时,内核会定义应用程序的命令计划任务(shedule方法中定义的计划任务)publicfunction__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();});}Application解析consolekernel查看aritisan文件源码,可以看到consolekernel绑定后完成后,会通过服务容器对象解析控制台内核$kernel=$app->make(Illuminate\Contracts\Console\Kernel::class);$status=$kernel->handle($input=新的Symfony\Component\Console\Input\ArgvInput,新的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程序引用应用程序一样,在开始处理命令任务之前也会有一个bootstrap应用程序。父类“IlluminateFoundationConsoleKernel”内部为“bootstrappers”定义了属性名Bootstrapper数组:protected$bootstrappers=[\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,\Illuminate\Foundation\Bootstrap\HandleExceptions::类,\Illuminate\Foundation\Bootstrap\RegisterFacades::类,\Illuminate\Foundation\Bootstrap\SetRequestForConsole::类,\Illuminate\Foundation\Bootstrap\RegisterProviders::类,\Illuminate\Foundation\Bootstrap\BootProviders::类,];数组中包含的bootloader与HTTP内核中定义的bootloader基本相同。它们都是在应用程序的初始化阶段、配置文件加载、异常处理器注册、控制台请求设置、应用程序中的服务容器注册、Facade和启动服务时要执行的环境变量。其中,设置Consolerequest是唯一不同于HTTP内核的启动程序。执行命令执行命令是通过ConsoleApplication执行的,ConsoleApplication继承自Symfony框架的Symfony\Component\Console\Application类,通过相应的run方法执行命令。名称Illuminate\Foundation\Console;classKernelimplementsKernelContract{publicfunctionhandle($input,$output=null){try{$this->bootstrap();返回$this->getArtisan()->run($input,$output);}catch(Exception$e){$this->reportException($e);$this->renderException($output,$e);返回1;}catch(Throwable$e){$e=newFatalThrowableError($e);$this->reportException($e);$this->renderException($output,$e);返回1;}}}namespaceSymfony\Component\Console;classApplication{//执行命令publicfunctionrun(InputInterface$input=null,OutputInterface$output=null){......try{$exitCode=$this->doRun($输入,$输出);}catch{......}......return$exitCode;}公共函数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'=>newInputArgument('command',InputArgument::OPTIONAL,$definition->getArgument('command')->getDescription(),$名字),)));}......try{//通过命令名找出命令类(命名空间、类名等)$command=$this->find($name);}......//运行命令class$exitCode=$this->doRunCommand($command,$input,$output);返回$exitCode;}protectedfunctiondoRunCommand(Command$command,InputInterface$input,OutputInterface$output){......//执行命令类run方法处理任务$exitCode=$command->run($input,$output);......返回$exitcode;}}执行一个命令主要分为三个步骤:解析出命令名和Parameteroptions通过命令名查找命令类的命名空间和类名。执行命令类的run方法完成任务处理并返回状态码。和命令行脚本的规范一样,如果命令任务程序执行成功,则返回0,如果抛出异常,则返回1。另外,打开命令类后,我们可以看到有没有运行方法。我们把处理逻辑写在handle方法中。仔细看代码会发现父类中定义了run方法,子类会在run方法会上调用。中定义的handle方法完成任务处理。严格遵循面向对象编程的SOLID原则。结束应用执行命令程序返回状态码后,工匠会直接通过exit($status)函数输出状态码并结束PHP进程,然后shell进程会根据脚本命令判断是否执行返回的状态码是否为0成功。通过命令行启动的程序过程到这里结束。和HTTP内核一样,Console内核也负责整个生命周期的调度,但是Http内核最终将请求发送给Controller程序,而Console内核发送命令行请求落地在Laravel中定义的各种命令程序,然后在命令类中,我们可以像其他程序一样自由使用Laravel中的各种组件和服务容器中注册的服务。本文已收录在Laravel源码学习系列文章中。也欢迎大家关注我的公众号网管闲聊。最近准备分享一些在日常工作中学习和总结的技术知识,也会分享一些学习英语的知识和方法。
