原始碼分析laravel artisan命令列

oliver-l發表於2020-12-30

在使用laravel進行開發,我們會經常用到laravel的命令列php artisan去執行一些操作,如生成檔案,啟動佇列,執行資料庫遷移等。這讓我有點好奇artisan的工作原理是怎樣的。所以想通過閱讀原始碼嘗試分析其工作原理,當然如果有錯誤的地方,歡迎指出。

在laravel專案的一級目錄會有artisan.php檔案,這是執行php artisan的入口檔案,程式碼非常簡潔,就只有幾行程式碼就可以讓我們實現命令列的功能。

//宣告php指令碼,跟shell指令碼中的#!/bin/bash宣告類似
#!/usr/bin/env php

<?php

//引入自動載入機制
require __DIR__.'/bootstrap/autoload.php';

//引入laravel容器核心
$app = require_once __DIR__.'/bootstrap/app.php';

//例項化控制檯核心類
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);

//執行php artisan的命令
$status = $kernel->handle(
    $input = new Symfony\Component\Console\Input\ArgvInput,
    new Symfony\Component\Console\Output\ConsoleOutput
);

//結束命令列程式
$kernel->terminate($input, $status);

exit($status);

其中$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);例項化的核心類是在./bootstrap/app.php中通過下面程式碼繫結的單例物件。完成繫結後可通過make()方法將繫結的物件通過反射類的形式例項化。

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

其中./app/Console/Kenel.php控制檯核心類繼承了Illuminate\Foundation\Console\Kernel類,這個繼承的類為執行php artisan的核心類,我們檢視Kernel類中的handle方法。

//其中$input,$output分別為Symfony\Component\Console\Input\ArgvInput類和Symfony\Component\Console\Output\ConsoleOutput分別用於處理引數的輸入和結果的輸出
public function handle($input, $output = null)
    {
        try {
            /**
            *將$bootstrappers陣列中的定義的類繫結到容器中,並執行bootstrap()初始化類
            *包括初始化載入env,載入config配置,載入錯誤處理機制,註冊門面類,載入服務提供者等
            **/
            $this->bootstrap();
            //獲取artisan例項並執行run()方法
            return $this->getArtisan()->run($input, $output);
        } catch (Throwable $e) {
            //異常捕獲
            $this->reportException($e);
            $this->renderException($output, $e);

            return 1;
        }
    }

檢視artisan例項的run(),例項通過use Illuminate\Console\Application as Artisan;引入;其中$exitCode = parent::run($input, $output);為執行的核心,通過繼承Symfony\Component\Console\Application類以實現其核心功能

    /**
     * {@inheritdoc}
     */
    public function run(InputInterface $input = null, OutputInterface $output = null)
    {
        //獲取命令,如執行php artisan make:controller AdminController,則獲取make:controller命令
        $commandName = $this->getCommandName(
            $input = $input ?: new ArgvInput
        );
        //新增CommandStarting分發事件
        $this->events->dispatch(
            new CommandStarting(
                $commandName, $input, $output = $output ?: new ConsoleOutput
            )
        );
        //執行命令
        $exitCode = parent::run($input, $output);
        //新增CommandFinished分發事件
        $this->events->dispatch(
            new CommandFinished($commandName, $input, $output, $exitCode)
        );

        return $exitCode;
    }

檢視Symfony\Component\Console\Application類中的run()方法,這裡簡單說明一下這個run(),其執行的核心如下

...程式碼省略...
//基於使用者引數和選項配置輸入和輸出例項。
$this->configureIO($input, $output);

try {
    //執行命令
    $exitCode = $this->doRun($input, $output);
} catch (\Exception $e) {
    //異常捕獲,對不存在的命令或執行出錯的命令進行捕獲處理輸出
    if (!$this->catchExceptions) {
        throw $e;
    }
    //執行定義的閉包函式
    $renderException($e);
    ...程式碼省略...
}

$renderException($e);
...程式碼省略...

再來看看doRun()方法的執行

public function doRun(InputInterface $input, OutputInterface $output)
    {
        //檢查是否包含--version或-V兩個引數,若存在則無論則直接返回laravel的版本號
        if (true === $input->hasParameterOption(['--version', '-V'], true)) {
            $output->writeln($this->getLongVersion());

            return 0;
        }

        try {
            //繫結命令的基本引數,如執行命令php artisan make:migration create_users_table --create=users對其中的引數進行解析繫結
            $input->bind($this->getDefinition());
        } catch (ExceptionInterface $e) {
            //異常捕獲,使程式即使報錯也能正常執行
        }
        //獲取命令名稱,若執行php artisan make:migration create_users_table --create=users命令則獲取make:migration
        $name = $this->getCommandName($input);
        //判斷是否包含--help或-h引數
        if (true === $input->hasParameterOption(['--help', '-h'], true)) {
            if (!$name) {
                //若命令為空,php artisan等價於php artisan help
                $name = 'help';
                $input = new ArrayInput(['command_name' => $this->defaultCommand]);
            } else {
                $this->wantHelps = true;
            }
        }
        //判斷命令為空,為其設定相關引數
        if (!$name) {
            $name = $this->defaultCommand;
            $definition = $this->getDefinition();
            $definition->setArguments(array_merge(
                $definition->getArguments(),
                [
                    'command' => new InputArgument('command', InputArgument::OPTIONAL, $definition->getArgument('command')->getDescription(), $name),
                ]
            ));
        }

        try {
            $this->runningCommand = null;
            //獲取命令具體詳情,相當於一個工廠函式
            $command = $this->find($name);
        } catch (\Throwable $e) {
            //對異常命令或錯誤命令進行處理
            if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) || 1 !== \count($alternatives = $e->getAlternatives()) || !$input->isInteractive()) {
                ...程式碼省略...
        }

        $this->runningCommand = $command;
        //再次執行命令
        $exitCode = $this->doRunCommand($command, $input, $output);
        $this->runningCommand = null;

        return $exitCode;
    }

這裡檢視工廠函式find()方法

public function find(string $name)
    {
        //初始化命令,相當於為$this->commands賦值所有命令類,其key值為命令名稱,value為具體命令類例項
        $this->init();

        $aliases = [];
        //遍歷$this->commands將命令別名也新增到$this->commands變數中
        foreach ($this->commands as $command) {
            //dump('命令名稱:'.$command->getName().PHP_EOL);
            //dump('類名稱:'.get_class($command));
            foreach ($command->getAliases() as $alias) {
                if (!$this->has($alias)) {
                    $this->commands[$alias] = $command;
                }
            }
        }
        //判斷命令是否存在
        if ($this->has($name)) {
            //命令存在則返回當前命令的命令類
            return $this->get($name);
        }
        ...程式碼省略...
    }

這裡執行命令,dump列印輸出一下相關資訊,如下所示

原始碼分析laravel artisan命令列

往下檢視doRunCommand()方法,當程式能夠進入到這個函式,說明當前的命令是存在的。再檢視其程式碼流程如下

//$command為具體的命令類
protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
    {
        //設定一些不清楚是啥的helper,應該是命令幫助設定吧
        foreach ($command->getHelperSet() as $helper) {
            if ($helper instanceof InputAwareInterface) {
                $helper->setInput($input);
            }
        }
        if (null === $this->dispatcher) {
            /**
            **具體核心是執行$command->run()
            **以php artisan cache:clear為例,這裡例項的命令類為Illuminate\Cache\Console\ClearCommand,每一個命令類其底層都繼承了Symfony\Component\Console\Command\Command這個基礎命令類,這裡執行的run()就是由這裡繼承而已,但最終執行每個命令類具體操作是執行具體命令類中的handle()方法
            **/
            return $command->run($input, $output);
        }
        ...程式碼省略...
    }

以上就是我對於laravel artisan命令列的原始碼分析。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章