如何理解laravel的服务加载流程

加载流程

所有的解读都将以注释的方式出现。

  1. 文件流程

    • 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
  2. 代码流程

    • 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']);
          }

服务挖掘

  1. 这个功能得益于post-autoload-dump事件,composer会在依赖安装完成后执行一个叫做@php artisan package:discover --ansi的命令,去查找是否有laravel相关的服务需要注册,如果有那就把服务信息记录在bootstrap/cache/services.php

  2. 如果不依赖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",这样就只发布配置文件到正式目录。

  • 参阅:

    1. \Illuminate\Foundation\Console\VendorPublishCommand::publishTag()
    2. \Illuminate\Support\ServiceProvider::pathsToPublish()

如何理解laravel的服务加载流程
http://blog.icy8.cn/posts/5809/
作者
icy8
发布于
2022年2月25日
许可协议