看 Laravel 原始碼瞭解 ServiceProvider 的載入

coding01發表於2018-06-12

使用 Laravel 開始,我們總是繞不開 ServiceProvider 這個概念。在 Laravel 框架裡到處充斥著 ServiceProvider —— AppServiceProvider、AuthServiceProvider、BroadcastServiceProvider、EventServiceProvider 和 RouteServiceProvider 等等。

還有我們在安裝一些第三方外掛時,都時不時有這麼句話,將****ServiceProvider 加入到 config/app.php 的 providers 陣列中。

看 Laravel 原始碼瞭解 ServiceProvider 的載入

難道我們們就不想知道 Laravel 是如何載入這些 ServiceProviders 的嗎?

所以今天從原始碼的執行來看看是怎麼實現載入的?

複製程式碼

看 Application 類

我們都知道 Laravel 的入口檔案在 public/index.php

<?php
...
require __DIR__.'/../vendor/autoload.php';
...
$app = require_once __DIR__.'/../bootstrap/app.php';

/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);

複製程式碼

這裡先看載入 require_once __DIR__.'/../bootstrap/app.php',建立 app 物件。

<?php

/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| The first thing we will do is create a new Laravel application instance
| which serves as the "glue" for all the components of Laravel, and is
| the IoC container for the system binding all of the various parts.
|
*/

$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
|
| Next, we need to bind some important interfaces into the container so
| we will be able to resolve them when needed. The kernels serve the
| incoming requests to this application from both the web and CLI.
|
*/

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

/*
|--------------------------------------------------------------------------
| Return The Application
|--------------------------------------------------------------------------
|
| This script returns the application instance. The instance is given to
| the calling script so we can separate the building of the instances
| from the actual running of the application and sending responses.
|
*/

return $app;
複製程式碼

直接返回的是 new Illuminate\Foundation\Application( realpath(__DIR__.'/../') Application 物件。

這個物件就是 Laravel 的「容器」。我們開始看看 Application 是怎麼發現 ServiceProvider 的?

/**
 * Create a new Illuminate application instance.
 *
 * @param  string|null  $basePath
 * @return void
 */
public function __construct($basePath = null)
{
    if ($basePath) {
        $this->setBasePath($basePath);
    }

    $this->registerBaseBindings();

    $this->registerBaseServiceProviders();

    $this->registerCoreContainerAliases();
}
複製程式碼

主要是完成這四個方法。第一個和最後一個方法暫且不表;我們主要看:

$this->registerBaseBindings(); $this->registerBaseServiceProviders();

registerBaseBindings()

/**
 * Register the basic bindings into the container.
 *
 * @return void
 */
protected function registerBaseBindings()
{
    static::setInstance($this);

    $this->instance('app', $this);

    $this->instance(Container::class, $this);

    $this->instance(PackageManifest::class, new PackageManifest(
        new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
    ));
}
複製程式碼

前兩個主要是繫結 Application 物件和 Container 物件。重點分析 PackageManifest 物件之前,我們看看 $this->getCachedPackagesPath()這個函式:

/**
 * Get the path to the cached packages.php file.
 *
 * @return string
 */
public function getCachedPackagesPath()
{
    return $this->bootstrapPath().'/cache/packages.php';
}
複製程式碼

這個就是我們 bootstrap/cache/packages.php檔案,這個檔案的內容我們看看:

<?php return array (
  'fideloper/proxy' => 
  array (
    'providers' => 
    array (
      0 => 'Fideloper\\Proxy\\TrustedProxyServiceProvider',
    ),
  ),
  'encore/laravel-admin' => 
  array (
    'providers' => 
    array (
      0 => 'Encore\\Admin\\AdminServiceProvider',
    ),
    'aliases' => 
    array (
      'Admin' => 'Encore\\Admin\\Facades\\Admin',
    ),
  ),
  'laravel/tinker' => 
  array (
    'providers' => 
    array (
      0 => 'Laravel\\Tinker\\TinkerServiceProvider',
    ),
  ),
  'rebing/graphql-laravel' => 
  array (
    'providers' => 
    array (
      0 => 'Rebing\\GraphQL\\GraphQLServiceProvider',
    ),
    'aliases' => 
    array (
      'GraphQL' => 'Rebing\\GraphQL\\Support\\Facades\\GraphQL',
    ),
  ),
  'tymon/jwt-auth' => 
  array (
    'aliases' => 
    array (
      'JWTAuth' => 'Tymon\\JWTAuth\\Facades\\JWTAuth',
      'JWTFactory' => 'Tymon\\JWTAuth\\Facades\\JWTFactory',
    ),
    'providers' => 
    array (
      0 => 'Tymon\\JWTAuth\\Providers\\LaravelServiceProvider',
    ),
  ),
  'noh4ck/graphiql' => 
  array (
    'providers' => 
    array (
      0 => 'Graphiql\\GraphiqlServiceProvider',
    ),
  ),
  'rollbar/rollbar-laravel' => 
  array (
    'providers' => 
    array (
      0 => 'Rollbar\\Laravel\\RollbarServiceProvider',
    ),
    'aliases' => 
    array (
      'Rollbar' => 'Rollbar\\Laravel\\Facades\\Rollbar',
    ),
  ),
  'fanly/log2dingding' => 
  array (
    'providers' => 
    array (
      0 => 'Fanly\\Log2dingding\\FanlyLog2dingdingServiceProvider',
    ),
  ),
);
複製程式碼

通過分析,可以看出這個檔案主要是放著我們自己引入第三方的 ServiceProvidersaliases,我們對照專案根路徑的 composer.json 你就可以證實了:

"require": {
    "php": ">=7.0.0",
    "encore/laravel-admin": "1.5.*",
    "fanly/log2dingding": "^0.0.2",
    "fideloper/proxy": "~3.3",
    "guzzlehttp/guzzle": "^6.3",
    "laravel/framework": "5.5.*",
    "laravel/tinker": "~1.0",
    "noh4ck/graphiql": "@dev",
    "overtrue/phplint": "^1.1",
    "rebing/graphql-laravel": "^1.10",
    "rollbar/rollbar-laravel": "^2.3",
    "tymon/jwt-auth": "^1.0@dev"
},
複製程式碼

至於這個 bootstrap/cache/packages.php 檔案內容怎麼產生的,我們後面會說到。

我們回來分析 new PackageManifest(),類中的幾個函式的作用,顯而易見:

/**
 * 這個函式是將 package.php 檔案的外掛陣列的 `providers`整合成一個 Collection 輸出
 */
public function providers() {}
    
/**
 * 外掛中的 `aliases` 整合成 Collection 輸出。
 *
 * @return array
 */
public function aliases() {}

/**
 * 這個是關鍵,從 verdor/composer/installed.json 檔案中獲取所有通過 composer 安裝的外掛陣列,然後再通過用 `name` 繫結對應的 `ServiceProvider`,構成陣列,然後再排除每個外掛的 `dont-discover` 和專案 composer.json 填入的 `dont-discover`。

 * 這也是 Laravel 包自動發現的核心所在。
 * 
 */
public function build()
{
    $packages = [];

    if ($this->files->exists($path = $this->vendorPath.'/composer/installed.json')) {
        $packages = json_decode($this->files->get($path), true);
    }

    $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());
}

/**
 * 最後就把上面的滿足的 ServiceProvider 寫入到檔案中,就是上文我們說的 `bootstrap/cache/packages.php`
 */
protected function write(array $manifest) {}
複製程式碼

到目前為止,我們找到了需要載入的第三方的 ServiceProvider 了。

registerBaseServiceProviders()

接下來我們看看這個 registerBaseServiceProviders() 方法了。

/**
 * Register all of the base service providers.
 *
 * @return void
 */
protected function registerBaseServiceProviders()
{
    $this->register(new EventServiceProvider($this));

    $this->register(new LogServiceProvider($this));

    $this->register(new RoutingServiceProvider($this));
}
複製程式碼

這裡主要註冊三個 ServiceProvider,具體功能後面詳聊。

Kernel

我們簡單過了一遍 new Application,我們回到 index.php 繼續往下看:

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();
複製程式碼

這個 $kernel 就是 Laravel 的「核」,而 $kernel->handle() 方法就是 Laravel 的「核中之核」了 —— 即,根據輸入的 Request,輸出 response。完成請求到響應的過程。

我們進入 $kernel->handle() 方法。

/**
 * Handle an incoming HTTP request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
public function handle($request)
{
    try {
        $request->enableHttpMethodParameterOverride();

        $response = $this->sendRequestThroughRouter($request);
    } catch (Exception $e) {
        $this->reportException($e);

        $response = $this->renderException($request, $e);
    } catch (Throwable $e) {
        $this->reportException($e = new FatalThrowableError($e));

        $response = $this->renderException($request, $e);
    }

    $this->app['events']->dispatch(
        new Events\RequestHandled($request, $response)
    );

    return $response;
}
複製程式碼

排除其它「干擾」東西,眼睛關注到這行程式碼:

$response = $this->sendRequestThroughRouter($request);
複製程式碼

這也暴露了,網路請求的三要素:request、router 和 response。

/**
 * Send the given request through the middleware / router.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\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());
}
複製程式碼

但今天我們說的不是考慮執行的問題,我們需要知道什麼時候載入我們的 ServiceProviders 所以在 return 執行之前的程式碼 ($this->bootstrap();) 就是初始化 ServiceProviders等資訊的過程

/**
 * Bootstrap the application for HTTP requests.
 *
 * @return void
 */
public function bootstrap()
{
    if (! $this->app->hasBeenBootstrapped()) {
        $this->app->bootstrapWith($this->bootstrappers());
    }
}
    
// Application 類:

/**
 * Run the given array of bootstrap classes.
 *
 * @param  array  $bootstrappers
 * @return void
 */
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]);
    }
}
複製程式碼

到此我們知道,實際上遍歷執行 $bootstrappers->bootstrap($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,
    ];
複製程式碼

這六個類的作用主要是:載入環境變數、config、異常處理、註冊 facades、和最後我們關注的 ServiceProviderregisterboot

我們分別來看看。

RegisterProviders

class RegisterProviders
{
    /**
     * Bootstrap the given application.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function bootstrap(Application $app)
    {
        $app->registerConfiguredProviders();
    }
}
複製程式碼

實際上是呼叫 ApplicationregisterConfiguredProviders()

/**
 * Register all of the configured providers.
 *
 * @return void
 */
public function registerConfiguredProviders()
{
    $providers = Collection::make($this->config['app.providers'])
                    ->partition(function ($provider) {
                        return Str::startsWith($provider, 'Illuminate\\');
                    });

    $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);

    (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
                ->load($providers->collapse()->toArray());
}
複製程式碼

這個就厲害了。載入所有已配置的 ServiceProvider,主要包含了在配置檔案 config/app.php 裡的 providers,上文講述的第三方所有滿足的 ServiceProviders,以及在 boostrap/cached/service.php 中的所有 Providers

最後執行 ProviderRepository::load 方法,進行遍歷 register


/**
 * Register the application service providers.
 *
 * @param  array  $providers
 * @return void
 */
public function load(array $providers)
{
    $manifest = $this->loadManifest();

    // First we will load the service manifest, which contains information on all
    // service providers registered with the application and which services it
    // provides. This is used to know which services are "deferred" loaders.
    if ($this->shouldRecompile($manifest, $providers)) {
        $manifest = $this->compileManifest($providers);
    }

    // Next, we will register events to load the providers for each of the events
    // that it has requested. This allows the service provider to defer itself
    // while still getting automatically loaded when a certain event occurs.
    foreach ($manifest['when'] as $provider => $events) {
        $this->registerLoadEvents($provider, $events);
    }

    // We will go ahead and register all of the eagerly loaded providers with the
    // application so their services can be registered with the application as
    // a provided service. Then we will set the deferred service list on it.
    foreach ($manifest['eager'] as $provider) {
        $this->app->register($provider);
    }

    $this->app->addDeferredServices($manifest['deferred']);
}


/**
 * Register the load events for the given provider.
 *
 * @param  string  $provider
 * @param  array  $events
 * @return void
 */
protected function registerLoadEvents($provider, array $events)
{
    if (count($events) < 1) {
        return;
    }

    $this->app->make('events')->listen($events, function () use ($provider) {
        $this->app->register($provider);
    });
}
複製程式碼

register 之後,我們可以看看 boot 方法了。

BootProviders

class BootProviders
{
    /**
     * Bootstrap the given application.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function bootstrap(Application $app)
    {
        $app->boot();
    }
}
複製程式碼

我們按圖索驥:

/**
 * Boot the application's service providers.
 *
 * @return void
 */
public function boot()
{
    if ($this->booted) {
        return;
    }

    // Once the application has booted we will also fire some "booted" callbacks
    // for any listeners that need to do work after this initial booting gets
    // finished. This is useful when ordering the boot-up processes we run.
    $this->fireAppCallbacks($this->bootingCallbacks);

    array_walk($this->serviceProviders, function ($p) {
        $this->bootProvider($p);
    });

    $this->booted = true;

    $this->fireAppCallbacks($this->bootedCallbacks);
}

...

/**
 * Boot the given service provider.
 *
 * @param  \Illuminate\Support\ServiceProvider  $provider
 * @return mixed
 */
protected function bootProvider(ServiceProvider $provider)
{
    if (method_exists($provider, 'boot')) {
        return $this->call([$provider, 'boot']);
    }
}
複製程式碼

也就是說遍歷執行所有 ServiceProvidersboot() (前提是該 ServiceProvider 有定義該方法)。

總結

通過分析 index.php 執行過程,發現 Application 主要是發現各種 ServiceProviders,而 $kernel 更多的是在處理 Request請求之前,把所有的 ServiceProvider進行註冊 (register),然後再 boot。把所有的 ServiceProviders裝載進系統記憶體中,供處理各式各樣的 Request 使用。

而每一個 ServiceProvider 各司其職,負責各自不同的職能,來滿足 Laravel 系統的各種需要。

下文我們將重點說說這些 ServiceProvider 的含義和作用。敬請期待!

未完待續

相關文章