最近看了一下 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
和 $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
。