最近看了一下 laravel 這個框架,寫點東西當個筆記。跟着官網上的說明 install 好一個項目後,在項目根目錄執行命令php artisan serve
就能夠開啓一個簡易的服務器進行開發,這個命令到底作了什麼,看了一下代碼,在這裏簡要描述一下本身的見解。php
先說明一下,這裏項目 install 的方法不是安裝 laravel/installer,而是composer create-project --prefer-dist laravel/laravel blog
,寫筆記的時候 laravel
的版本仍是 5.5,之後版本更新後可能就不同了。laravel
artisan 其實是項目根目錄下的一個 php 腳本,並且默認是有執行權限的,因此命令其實能夠簡寫成artisan serve
,腳本的代碼行數不多,實際上就十幾行:bootstrap
#!/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
方法:app
<?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
方法以下:composer
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
方法是這樣的:ide
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
和 $devCommands
merge 在一塊兒後取出值的數組(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
。