资讯专栏INFORMATION COLUMN

"php artisan serve"到底干了什么

TANKING / 2097人阅读

摘要:最近看了一下这个框架,写点东西当个笔记。函数会迭代属性为的,逐一将其注册,的方法继承自父类,关键的就是在这个里注册的。

最近看了一下 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
make(IlluminateContractsConsoleKernel::class);

$status = $kernel->handle(
    $input = new SymfonyComponentConsoleInputArgvInput,
    new SymfonyComponentConsoleOutputConsoleOutput
);

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

exit($status);

代码里,require __DIR__."/vendor/autoload.php";的 autoload.php 文件是 composer 生成的文件,实际用处就是利用 php 提供 spl_autoload_register 函数注册一个方法,让执行时遇到一个未声明的类时会自动将包含类定义的文件包含进来,举个例子就是脚本当中并没有包含任何文件,但却可以直接 new 一个 SymfonyComponentConsoleInputArgvInput 对象,就是这个 autoload.php 的功劳了。

接下来的这一行,$app = require_once __DIR__."/bootstrap/app.php";,在脚本里实例化一个 IlluminateFoundationApplication 对象,将几个重要的接口和类绑定在一起,然后将 Application 对象返回,其中接下来用到的 IlluminateContractsConsoleKernel::class 就是在这里和 AppConsoleKernel::class 绑定在一起的。

$kernel = $app->make(IlluminateContractsConsoleKernel::class);,直观的解释就是让 $app 制造出一个 AppConsoleKernel::class 实例(虽然括号里是 IlluminateContractsConsoleKernel::class,但由于跟这个接口绑定在一起的是 AppConsoleKernel::class 所以实际上 $kernel 实际上是 AppConsoleKernel::class)。

之后的就是整个脚本中最重要的一行了,调用 $kernelhandle 方法,AppConsoleKernel::class这个类在项目根目录下的 app/Console 文件夹里,这个类并没有实现 handle 方法,实际上调用的是它的父类的 handle方法:


IlluminateFoundationConsoleKernelhandler 方法如下:

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 实际上就是之前实例化的 IlluminateFoundationApplication ,所以调用的 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 = [
    IlluminateFoundationBootstrapLoadEnvironmentVariables::class,
    IlluminateFoundationBootstrapLoadConfiguration::class,
    IlluminateFoundationBootstrapHandleExceptions::class,
    IlluminateFoundationBootstrapRegisterFacades::class,
    IlluminateFoundationBootstrapSetRequestForConsole::class,
    IlluminateFoundationBootstrapRegisterProviders::class,
    IlluminateFoundationBootstrapBootProviders::class,
];

其中 IlluminateFoundationBootstrapRegisterProviders::classbootstrap 会调用 IlluminateFoundationApplication 实例的 registerConfiguredProviders 方法,这个方法会将读取到的项目配置里的配置项(项目根目录下的 config/app.php 文件里的 providers)放入一个 IlluminateSupportCollection 对象中,然后和缓存合并并且排除掉其中的重复项作为一个 ProviderRepository 实例的 load 方法的参数,这个 load 方法里会将 $defer 属性不为 true 的 Provider 类使用 IlluminateFoundationApplicationregister 方法注册(最简单理解就是 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" 的方法,这些方法会在 IlluminateFoundationApplication 这个容器(即 IlluminateFoundationApplication 实例,这个类继承了 IlluminateContainerContainer)中注册命令,当需要使用这些命令时就会返回一个这些命令的实例:

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 IlluminateConsoleApplication 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 类(实际上是 IlluminateConsoleApplication 类,不过引入时起了个别名)的静态成员 $bootstrappers 里,这个会在接下来再提及到。

接下来回到 IlluminateFoundationConsoleKernelhandler 方法,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 方法实际上还是调用了父类(SymfonyComponentConsoleApplication)的 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 EventsCommandStarting(
            $commandName, $input, $output = $output ?: new ConsoleOutput
        )
    );

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

    $this->events->fire(
        new EventsCommandFinished($commandName, $input, $output, $exitCode)
    );

    return $exitCode;
}

$input 参数是在 artisan 脚本里 new 出来的 SymfonyComponentConsoleInputArgvInput 对象,getCommandName 是继承自父类的方法:

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

也就是说这个方法的返回结果就是 SymfonyComponentConsoleInputArgvInput 对象的 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 没有实现该方法,所以其实是调用父类 IlluminateConsoleCommandrun,但父类的方法实际也只有一行,那就是调用其父类的 run,所以贴出来的其实是 SymfonyComponentConsoleCommandCommandrun):

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 没有实现该方法,IlluminateConsoleCommandexecute 方法如下:

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

也就是调用了 ServeCommandhandle 方法:

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

    $this->line("Laravel development server started: 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://www.ucloud.cn/yun/28181.html

相关文章

  • Laravel Artisan 命令

    摘要:显示帮助信息强制输出禁用输出 Laravel Framework version 5.1.3 (LTS) Usage: command [options] [arguments] Options: -h, --help 显示帮助信息 -q, --quiet Do not output any message -V, --ve...

    txgcwm 评论0 收藏0
  • Vue报错SyntaxError:TypeError:this.getOptionsisnotafunction的解决方法

      一、简单介绍  Vue 开发中会出现一些问题,比如:Vue报错SyntaxError:TypeError:this.getOptionsisnotafunction,要如何解决?  二、报错现象  ERROR Failed to compile with 1 error 上午10:39:05  error in ./src/views/Login.vue?vue&type=style&...

    3403771864 评论0 收藏0
  • Laravel 5.4 入门系列 1. 安装

    摘要:的安装与使用是什么是的一个依赖管理工具。它以项目为单位进行管理,你只需要声明项目所依赖的代码库,会自动帮你安装这些代码库。 Composer 的安装与使用 Composer 是什么 Composer 是 PHP 的一个依赖管理工具。它以项目为单位进行管理,你只需要声明项目所依赖的代码库,Composer 会自动帮你安装这些代码库。 安装 Composer Mac 下的安装只需要在命令行...

    hqman 评论0 收藏0
  • 源码解读:php artisan serve

    摘要:原文来自在学习的时候,可能很多人接触的第一个的命令就是,这样我们就可以跑起第一个的应用。本文来尝试解读一下这个命令行的源码。 原文来自:https://www.codecasts.com/blo... 在学习 Laravel 的时候,可能很多人接触的第一个 artisan 的命令就是:php artisan serve,这样我们就可以跑起第一个 Laravel 的应用。本文来尝试解读一...

    Loong_T 评论0 收藏0
  • 降低vue-router版本的2种解决方法实例

      在Vue.js官方的路由插件中,vue-router和vue.js是深度集成的,这类页面适合用于构建单页面应用。但要注意是由于无法注明版本,一般就默认安装router4.X,但我们创建的是vue2,只能结合 vue-router 3.x 版本才能使用。现在需要降低版本。  方法  我们知道vue-router 4.x 只能结合 vue3 进行使用,vue-router 3.x 只能结合 vue...

    3403771864 评论0 收藏0

发表评论

0条评论

TANKING

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<