您的位置:首页 > 编程语言 > PHP开发

"php artisan serve"到底干了什么

2018-03-30 08:01 501 查看
最近看了一下 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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: