Laravel 原始碼閱讀指南 -- Console 核心

KevinYan發表於2018-12-01

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程式引用應用一樣在開始處理命令任務之前也會有引導應用這一步操作

其父類 「Illuminate\Foundation\Console\Kernel」 內部定義了屬性名為 「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核心程式碼學習中,歡迎訪問閱讀,多多交流。

關注微信公眾號網管叨bi叨不錯過每一篇好文

相關文章