"php artisan serve"到底幹了什麼

最近看了一下 laravel 這個框架,寫點東西當個筆記。跟着官網上的說明 install 好一個項目後,在項目根目錄執行命令php artisan serve就可以開啓一個簡易的服務器進行開發,這個命令到底做了什麼,看了一下代碼,在這裏簡要描述一下自己的看法。

先說明一下,這裏項目 install 的方法不是安裝 laravel/installer,而是composer create-project --prefer-dist laravel/laravel blog,寫筆記的時候 laravel 的版本還是 5.5,以後版本更新後可能就不一樣了。

artisan 實際上是項目根目錄下的一個 php 腳本,而且默認是有執行權限的,所以命令其實可以簡寫成artisan serve,腳本的代碼行數很少,實際上就十幾行:

#!/usr/bin/env php
<?php

define('LARAVEL_START', microtime(true));

require __DIR__.'/vendor/autoload.php';

$app = require_once __DIR__.'/bootstrap/app.php';

$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);

$status = $kernel->handle(
    $input = new Symfony\Component\Console\Input\ArgvInput,
    new Symfony\Component\Console\Output\ConsoleOutput
);

$kernel->terminate($input, $status);

exit($status);

代碼裏,require __DIR__.'/vendor/autoload.php';的 autoload.php 文件是 composer 生成的文件,實際用處就是利用 php 提供 spl_autoload_register 函數註冊一個方法,讓執行時遇到一個未聲明的類時會自動將包含類定義的文件包含進來,舉個例子就是腳本當中並沒有包含任何文件,但卻可以直接 new 一個 Symfony\Component\Console\Input\ArgvInput 對象,就是這個 autoload.php 的功勞了。

接下來的這一行,$app = require_once __DIR__.'/bootstrap/app.php';,在腳本里實例化一個 Illuminate\Foundation\Application 對象,將幾個重要的接口和類綁定在一起,然後將 Application 對象返回,其中接下來用到的 Illuminate\Contracts\Console\Kernel::class 就是在這裏和 App\Console\Kernel::class 綁定在一起的。

$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);,直觀的解釋就是讓 $app 製造出一個 App\Console\Kernel::class 實例(雖然括號裏是 Illuminate\Contracts\Console\Kernel::class,但由於跟這個接口綁定在一起的是 App\Console\Kernel::class 所以實際上 $kernel 實際上是 App\Console\Kernel::class)。

之後的就是整個腳本中最重要的一行了,調用 $kernel 的 handle 方法,App\Console\Kernel::class這個類在項目根目錄下的 app/Console 文件夾裏,這個類並沒有實現 handle 方法,實際上調用的是它的父類的 handle方法:

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
......
}

而 Illuminate\Foundation\Console\Kernel 的 handler 方法如下:

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;
    }
}

bootstrap 方法如下:

public function bootstrap()
{
    if (! $this->app->hasBeenBootstrapped()) {
        $this->app->bootstrapWith($this->bootstrappers());
    }

    $this->app->loadDeferredProviders();

    if (! $this->commandsLoaded) {
        $this->commands();

        $this->commandsLoaded = true;
    }
}

先從 bootstrap 方法說起, $kernel 對象裏的成員 $app 實際上就是之前實例化的 Illuminate\Foundation\Application ,所以調用的 bootstrapWith 方法是這樣的:

public function bootstrapWith(array $bootstrappers)
{
    $this->hasBeenBootstrapped = true;

    foreach ($bootstrappers as $bootstrapper) {
        $this['events']->fire('bootstrapping: '.$bootstrapper, [$this]);

        $this->make($bootstrapper)->bootstrap($this);

        $this['events']->fire('bootstrapped: '.$bootstrapper, [$this]);
    }
}

那麼串聯起來實際上 bootstrap 方法裏的這一句 $this->app->bootstrapWith($this->bootstrappers()); 就是實例化了 $kernel裏 $bootstrappers 包含的所有類並且調用了這些對象裏的 bootstrap 方法:

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,
];

其中 \Illuminate\Foundation\Bootstrap\RegisterProviders::class 的 bootstrap 會調用 Illuminate\Foundation\Application實例的 registerConfiguredProviders 方法,這個方法會將讀取到的項目配置裏的配置項(項目根目錄下的 config/app.php 文件裏的 providers)放入一個 Illuminate\Support\Collection 對象中,然後和緩存合併並且排除掉其中的重複項作爲一個 ProviderRepository 實例的 load 方法的參數,這個 load 方法裏會將 $defer 屬性不爲 true 的 Provider 類使用 Illuminate\Foundation\Application 的 register 方法註冊(最簡單理解就是 new 一個該 Provider 對象然後調用該對象的 register 方法)。

對 artisan 十分重要的一個 Provider (ArtisanServiceProvider)的註冊過程非常繞。

項目根目錄下的 config/app.php 裏有個 ConsoleSupportServiceProvider, $defer 屬性爲 true ,所以不會在上面提到的過程中馬上註冊,而會在 bootstrap 中的這句 $this->app->loadDeferredProviders(); 裏註冊。

loadDeferredProviders 函數會迭代 $defer 屬性爲 true 的 Provider,逐一將其註冊,ConsoleSupportServiceProvider 的 register 方法繼承自父類 AggregateServiceProvider ,關鍵的 ArtisanServiceProvider 就是在這個 register 裏註冊的。

ArtisanServiceProvider 的 register 方法如下:

public function register()
{
    $this->registerCommands(array_merge(
        $this->commands, $this->devCommands
    ));
}

protected function registerCommands(array $commands)
{
    foreach (array_keys($commands) as $command) {
        call_user_func_array([$this, "register{$command}Command"], []);
    }

    $this->commands(array_values($commands));
}

這個方法會調用自身的方法 registerCommands , registerCommands 會調用 ArtisanServiceProvider 裏所有名字類似 "register{$command}Command" 的方法,這些方法會在 Illuminate\Foundation\Application 這個容器(即 Illuminate\Foundation\Application 實例,這個類繼承了 Illuminate\Container\Container)中註冊命令,當需要使用這些命令時就會返回一個這些命令的實例:

protected function registerServeCommand()
{
    $this->app->singleton('command.serve', function () {
        return new ServeCommand;
    });
}

以 serve 這個命令爲例,這個方法的用處就是當需要從容器裏取出 command.serve 時就會得到一個 ServeCommand 實例。

registerCommands 方法裏還有一個重要的方法調用, $this->commands(array_values($commands)); , ArtisanServiceProvider裏並沒有這個方法的聲明,所以這個方法其實是在其父類 ServiceProvider 實現的:

use Illuminate\Console\Application as Artisan;

......

public function commands($commands)
{
    $commands = is_array($commands) ? $commands : func_get_args();

    Artisan::starting(function ($artisan) use ($commands) {
        $artisan->resolveCommands($commands);
    });
}

Artisan::starting 這個靜態方法的調用會將括號裏的匿名函數添加到 Artisan 類(實際上是 Illuminate\Console\Application類,不過引入時起了個別名)的靜態成員 $bootstrappers 裏,這個會在接下來再提及到。

接下來回到 Illuminate\Foundation\Console\Kernel 的 handler 方法,return $this->getArtisan()->run($input, $output);, getArtisan 方法如下:

protected function getArtisan()
{
    if (is_null($this->artisan)) {
        return $this->artisan = (new Artisan($this->app, $this->events, $this->app->version()))
                            ->resolveCommands($this->commands);
    }

    return $this->artisan;
}

該方法會 new 出一個 Artisan 對象, 而這個類會在自己的構造函數調用 bootstrap 方法:

protected function bootstrap()
{
    foreach (static::$bootstrappers as $bootstrapper) {
        $bootstrapper($this);
    }
}

這時候剛纔被提及到的匿名函數就是在這裏發揮作用,該匿名函數的作用就是調用 Artisan 對象的 resolveCommands 方法:

public function resolve($command)
{
    return $this->add($this->laravel->make($command));
}

public function resolveCommands($commands)
{
    $commands = is_array($commands) ? $commands : func_get_args();

    foreach ($commands as $command) {
        $this->resolve($command);
    }

    return $this;
}

resolveCommands 方法中迭代的 $commands 參數實際上是 ArtisanServiceProvider 裏的兩個屬性 $commands 和 $devCommandsmerge 在一起後取出值的數組(merge 發生在 ArtisanServiceProvider 的 register 方法, registerCommands 中使用 array_values 取出其中的值),所以對於 serve 這個命令,實際上發生的是 $this->resolve('command.serve');,而在之前已經提到過,ArtisanServiceProvider 的 "register{$command}Command" 的方法會在容器裏註冊命令,那麼 resolve 方法的結果將會是將一個 new 出來 ServeCommand 對象作爲參數被傳遞到 add 方法:

public function add(SymfonyCommand $command)
{
    if ($command instanceof Command) {
        $command->setLaravel($this->laravel);
    }

    return $this->addToParent($command);
}

protected function addToParent(SymfonyCommand $command)
{
    return parent::add($command);
}

add 方法實際上還是調用了父類(Symfony\Component\Console\Application)的 add

public function add(Command $command)
{
    ......

    $this->commands[$command->getName()] = $command;

    ......

    return $command;
}

關鍵在 $this->commands[$command->getName()] = $command;,參數 $command 已經知道是一個 ServeCommand 對象,所以這一句的作用就是在 Artisan 對象的 $commands 屬性添加了一個鍵爲 serve 、值爲 ServeCommand 對象的成員。

getArtisan 方法執行完後就會調用其返回的 Artisan 對象的 run 方法:

public function run(InputInterface $input = null, OutputInterface $output = null)
{
    $commandName = $this->getCommandName(
        $input = $input ?: new ArgvInput
    );

    $this->events->fire(
        new Events\CommandStarting(
            $commandName, $input, $output = $output ?: new ConsoleOutput
        )
    );

    $exitCode = parent::run($input, $output);

    $this->events->fire(
        new Events\CommandFinished($commandName, $input, $output, $exitCode)
    );

    return $exitCode;
}

$input 參數是在 artisan 腳本里 new 出來的 Symfony\Component\Console\Input\ArgvInput 對象,getCommandName 是繼承自父類的方法:

protected function getCommandName(InputInterface $input)
{
    return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument();
}

也就是說這個方法的返回結果就是 Symfony\Component\Console\Input\ArgvInput 對象的 getFirstArgument 方法的返回值:

public function __construct(array $argv = null, InputDefinition $definition = null)
{
    if (null === $argv) {
        $argv = $_SERVER['argv'];
    }

    // strip the application name
    array_shift($argv);

    $this->tokens = $argv;

    parent::__construct($definition);
}

......

public function getFirstArgument()
{
    foreach ($this->tokens as $token) {
        if ($token && '-' === $token[0]) {
            continue;
        }

        return $token;
    }
}

getFirstArgument 方法會將屬性 $tokens 裏第一個不包含 '-' 的成員返回,而 $tokens 屬性的值是在構造函數裏生成的,所以可以知道 getCommandName 的結果就是 serve 。

接下來 Artisan 對象調用了父類的 run 方法(篇幅太長,省略掉一點):

public function run(InputInterface $input = null, OutputInterface $output = null)
{
    ......

    try {
        $exitCode = $this->doRun($input, $output);
    } catch (\Exception $e) {
        if (!$this->catchExceptions) {
            throw $e;
    ......
}

public function doRun(InputInterface $input, OutputInterface $output)
{
    ......

    $name = $this->getCommandName($input);
    
    ......

    try {
        $e = $this->runningCommand = null;
        // the command name MUST be the first element of the input
        $command = $this->find($name);
    
    ......

    $this->runningCommand = $command;
    $exitCode = $this->doRunCommand($command, $input, $output);
    $this->runningCommand = null;

    return $exitCode;
}

protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
{
    ......

    if (null === $this->dispatcher) {
        return $command->run($input, $output);
    }

    ......
}

run 方法又會調用 doRun,而該方法會先使用 getCommandName 獲取到命令的名字('serve'),然後使用 find 方法找出與該命令對應的 Command 對象(在 $commands 屬性中查找,該屬性的結構類似 'serve' => 'ServeCommand'),被找出來的 Command 對象會被作爲參數傳遞到 doRunCommand 方法,最後在其中調用該對象的 run 方法(ServeCommand 沒有實現該方法,所以其實是調用父類 Illuminate\Console\Command 的 run,但父類的方法實際也只有一行,那就是調用其父類的 run,所以貼出來的其實是 Symfony\Component\Console\Command\Command 的 run):

public function run(InputInterface $input, OutputInterface $output)
{
    ......

    if ($this->code) {
        $statusCode = call_user_func($this->code, $input, $output);
    } else {
        $statusCode = $this->execute($input, $output);
    }

    return is_numeric($statusCode) ? (int) $statusCode : 0;
}

$code 並沒有賦值過,所以執行的是 $this->execute($input, $output);ServeCommand 沒有實現該方法,Illuminate\Console\Command 的 execute 方法如下:

protected function execute(InputInterface $input, OutputInterface $output)
{
    return $this->laravel->call([$this, 'handle']);
}

也就是調用了 ServeCommand 的 handle 方法:

public function handle()
{
    chdir($this->laravel->publicPath());

    $this->line("<info>Laravel development server started:</info> <http://{$this->host()}:{$this->port()}>");

    passthru($this->serverCommand());
}

protected function serverCommand()
{
    return sprintf('%s -S %s:%s %s/server.php',
        ProcessUtils::escapeArgument((new PhpExecutableFinder)->find(false)),
        $this->host(),
        $this->port(),
        ProcessUtils::escapeArgument($this->laravel->basePath())
    );
}

所以如果想打開一個簡易的服務器做開發,把目錄切換到根目錄的 public 目錄下,敲一下這個命令,效果是差不多的, php -S 127.0.0.1:8000 ../server.php

發佈了7 篇原創文章 · 獲贊 10 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章