最近看了一下 laravel 这个框架,写点东西当个笔记。跟着官网上的说明 install 好一个项目后,在项目根目录执行命令php artisan serve
就能够开启一个简易的服务器进行开发,这个命令到底作了什么,看了一下代码,在这里简要描述一下本身的见解。php
先说明一下,这里项目 install 的方法不是安装 laravel/installer,而是composer create-project --prefer-dist laravel/laravel blog
,写笔记的时候 laravel
的版本仍是 5.5,之后版本更新后可能就不同了。laravel
artisan 其实是项目根目录下的一个 php 脚本,并且默认是有执行权限的,因此命令其实能够简写成artisan serve
,脚本的代码行数不多,实际上就十几行:bootstrap
#!/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
方法:app
<?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
方法以下:composer
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
方法是这样的:ide
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
。