如何理解laravel的服务加载流程
加载流程
所有的解读都将以注释的方式出现。
文件流程
- public/index.php
- bootstrap/app.php
- app/Http/Kernel.php
- src/Illuminate/Foundation/Http/Kernel.php
- src/Illuminate/Foundation/Bootstrap/RegisterProviders.php
- src/Illuminate/Foundation/Bootstrap/BootProviders.php
- src/Illuminate/Foundation/Application.php
代码流程
public/index.php
web入口文件,console入口是
./artisan
<?php // ...... // 框架引导文件,就是入库文件 $app = require_once __DIR__.'/../bootstrap/app.php'; // 获取框架内核实例,这里是读到http的内核 $kernel = $app->make(Kernel::class); // 这里的handle()是框架内核的入口方法 // tap默认会将传入的对象的所有方法代理成链式(连贯)方法 $response = tap($kernel->handle( $request = Request::capture() ))->send();
bootstrap/app.php
框架引导文件
<?php // 应用容器的实例 $app = new Illuminate\Foundation\Application( $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) ); // 这个是向容器设置内核别名 // 这样做是为了在$app->make(Illuminate\Contracts\Http\Kernel::class)时能够直接获取到App\Http\Kernel::class的实例 // 在上一个文件中调用了Kernel->handle(),那下一个文件直接看App\Http\Kernel::class->handle()即可 $app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class ); // 单例,将实例闭包绑定在容器中,通过$app->make("classname")可以产生并获得实例 // 第一个参数可以理解为别名 // 第二个参数是被实例化的类 $app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class ); $app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class ); // ......
app/Http/Kernel.php
http内核,console内核在`app/Console/Kernel.php
<?php namespace App\Http; use Illuminate\Foundation\Http\Kernel as HttpKernel; // 这时候发现这个内核是定制过的,而且没有重写handle方法 // 所以需要进去父类看看handle方法做了什么 class Kernel extends HttpKernel { // ...... }
src/Illuminate/Foundation/Http/Kernel.php
接下来会通过分段来分析这个文件的代码
<?php // ... public function handle($request) { try { // 这个方法可以从字面意思理解是重写请求参数,就是过滤参数值 $request->enableHttpMethodParameterOverride(); // 这里返回了响应内容,可以进去看看 $response = $this->sendRequestThroughRouter($request); } catch (Throwable $e) { $this->reportException($e); $response = $this->renderException($request, $e); } $this->app['events']->dispatch( new RequestHandled($request, $response) ); return $response; } // ...... protected function sendRequestThroughRouter($request) { $this->app->instance('request', $request); Facade::clearResolvedInstance('request'); // 开始运行引导流程,接下来看这个方法 $this->bootstrap(); // 应用管道,会把响应内容逐级传递给中间件进行过滤 // 这里只分析服务加载,响应流程先不分析 return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter()); } // ...... public function bootstrap() { if (! $this->app->hasBeenBootstrapped()) { // 真正的启动又是另一个方法。。。盲猜他应该是make()->boot()之类的,先不看他了。 // 看起来bootstrappers()是获取引导文件列表的,那就先看他 $this->app->bootstrapWith($this->bootstrappers()); } } // ...... // 一堆引导流程 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\RegisterProviders::class, // 这里会启动系统服务 \Illuminate\Foundation\Bootstrap\BootProviders::class, ]; protected function bootstrappers() { // 写成方法可能是为了可扩展 // 因为$this->app->bootstrapWith()会从上到下执行引导文件的启动方法 // 所以根据引导顺序可以理解为:先注册服务,再去执行服务 return $this->bootstrappers; }
src/Illuminate/Foundation/Bootstrap/RegisterProviders.php
引导注册服务
<?php namespace Illuminate\Foundation\Bootstrap; use Illuminate\Contracts\Foundation\Application; class RegisterProviders { public function bootstrap(Application $app) { // 这个方法其实是在Illuminate\Foundation\Application,不要被参数类型干扰 // 因为Illuminate\Contracts\Foundation\Application被设置别名并指向app.php生成的容器实例了 $app->registerConfiguredProviders(); } }
src/Illuminate/Foundation/Bootstrap/BootProviders.php
按需启动服务
<?php namespace Illuminate\Foundation\Bootstrap; use Illuminate\Contracts\Foundation\Application; class BootProviders{ public function bootstrap(Application $app) { // 同上 $app->boot(); } }
src/Illuminate/Foundation/Application.php
应用容器,实例装载器。符合psr11
<?php // ...... public function registerConfiguredProviders() { // 将服务队列划为两部分,闭包是划分条件,[[...pass], [...fail]] $providers = Collection::make($this->config['app.providers']) ->partition(function ($provider) { return strpos($provider, 'Illuminate\\') === 0; }); // 向队列索引1后追加配置文件中的服务 // splice的length设为0代表向某个索引元素后追加内容 $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]); // 使用服务仓库向容器注册服务并缓存框架服务 (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath())) ->load($providers->collapse()->toArray()); } // ...... public function boot() { if ($this->isBooted()) { return; } $this->fireAppCallbacks($this->bootingCallbacks); // 服务注册过程会把实例存入容器serviceProviders属性中 array_walk($this->serviceProviders, function ($p) { // 转发到这个方法来启动服务 $this->bootProvider($p); }); $this->booted = true; $this->fireAppCallbacks($this->bootedCallbacks); } // ... protected function bootProvider(ServiceProvider $provider) { $provider->callBootingCallbacks(); if (method_exists($provider, 'boot')) { // 执行服务boot方法,至此服务流程结束 $this->call([$provider, 'boot']); } $provider->callBootedCallbacks(); }
src/Illuminate/Foundation/ProviderRepository.php
最后再来看看服务仓库是如何将服务注册到容器中去的
<?php // ...... public function load(array $providers) { // 先引入服务缓存文件,并返回服务资源结构 // 结构大概是这样的['providers' => $providers, 'eager' => [], 'deferred' => []] $manifest = $this->loadManifest(); // 编译服务资源,并缓存到文件 if ($this->shouldRecompile($manifest, $providers)) { $manifest = $this->compileManifest($providers); } // 事件服务,会在指定事件中向容器注册服务 foreach ($manifest['when'] as $provider => $events) { $this->registerLoadEvents($provider, $events); } // 这是指定要立即注册的服务 // 如果在应用引导启动后注册,那么服务被注册后将立即被执行 foreach ($manifest['eager'] as $provider) { $this->app->register($provider); } // 向容器注册延迟提供的服务,这些服务会在Console\Kernel引导程序之后执行 $this->app->addDeferredServices($manifest['deferred']); }
服务挖掘
这个功能得益于post-autoload-dump事件,composer会在依赖安装完成后执行一个叫做
@php artisan package:discover --ansi
的命令,去查找是否有laravel相关的服务需要注册,如果有那就把服务信息记录在bootstrap/cache/services.php
如果不依赖post-autoload-dump事件,如何找到依赖所要引入的服务?
// Illuminate\Foundation\PackageManifest // 每次注册服务都会执行一次PackageManifest->providers()->config('providers')->getManifest() // 如果manifest文件(就是bootstrap/cache/service.php)不存在就会往下执行build()方法 public function build() { $packages = []; if ($this->files->exists($path = $this->vendorPath.'/composer/installed.json')) { $installed = json_decode($this->files->get($path), true); $packages = $installed['packages'] ?? $installed; } $ignoreAll = in_array('*', $ignore = $this->packagesToIgnore()); $this->write(collect($packages)->mapWithKeys(function ($package) { return [$this->format($package['name']) => $package['extra']['laravel'] ?? []]; })->each(function ($configuration) use (&$ignore) { $ignore = array_merge($ignore, $configuration['dont-discover'] ?? []); })->reject(function ($configuration, $package) use ($ignore, $ignoreAll) { return $ignoreAll || in_array($package, $ignore); })->filter()->all()); }
发布资源
命令
php artisan vendor:publish --provider="..."
会将ServiceProvider::$publishes
队列中的文件/文件夹复制到映射目录,--tag
选项是指定发布分组资源。laravel-admin例子:
php artisan vendor:publish --tag="laravel-admin-config"
,这样就只发布配置文件到正式目录。参阅:
\Illuminate\Foundation\Console\VendorPublishCommand::publishTag()
\Illuminate\Support\ServiceProvider::pathsToPublish()