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