"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)。

之后的就是整个脚本中最重要的一行了,调用 $kernelhandle 方法,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\Kernelhandler 方法如下:

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::classbootstrap 会调用 Illuminate\Foundation\Application 实例的 registerConfiguredProviders 方法,这个方法会将读取到的项目配置里的配置项(项目根目录下的 config/app.php 文件里的 providers)放入一个 Illuminate\Support\Collection 对象中,然后和缓存合并并且排除掉其中的重复项作为一个 ProviderRepository 实例的 load 方法的参数,这个 load 方法里会将 $defer 属性不为 true 的 Provider 类使用 Illuminate\Foundation\Applicationregister 方法注册(最简单理解就是 new 一个该 Provider 对象然后调用该对象的 register 方法)。

artisan 十分重要的一个 ProviderArtisanServiceProvider)的注册过程非常绕。

项目根目录下的 config/app.php 里有个 ConsoleSupportServiceProvider$defer 属性为 true ,所以不会在上面提到的过程中马上注册,而会在 bootstrap 中的这句 $this->app->loadDeferredProviders(); 里注册。

loadDeferredProviders 函数会迭代 $defer 属性为 true 的 Provider,逐一将其注册,ConsoleSupportServiceProviderregister 方法继承自父类 AggregateServiceProvider ,关键的 ArtisanServiceProvider 就是在这个 register 里注册的。

ArtisanServiceProviderregister 方法如下:

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

这个方法会调用自身的方法 registerCommandsregisterCommands 会调用 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\Kernelhandler 方法,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 发生在 ArtisanServiceProviderregister 方法, 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\Commandrun,但父类的方法实际也只有一行,那就是调用其父类的 run,所以贴出来的其实是 Symfony\Component\Console\Command\Commandrun):

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\Commandexecute 方法如下:

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

也就是调用了 ServeCommandhandle 方法:

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

    原文作者:黄颖恒
    原文地址: https://segmentfault.com/a/1190000013025428
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞