PHP Laravel 的 Service Provider 理解
php
Lastmod: 2019-12-16

什麼是 Laravel Service Provider ?

上一篇文章『PHP Laravel 的 Container 理解』中咱們學習到了 Laravel 的 Container 是一種用來解決依賴與耦合的概念,它建立了一個容器並且在裡面定義好抽像與實際類別的對應,最後就可以自動的進行依賴性注入。如下偽程式碼。

<?php
$containter = require('Container');

// 建立抽象與實體類別的對應
$containter->bind(ILogService, AWSLogServcie::class);

$log = $container->make(Log::class);

$log->send('log....');

其中上面的bind就是可以在這個容器內建立一個抽象類別舉實體類別的對應,也就是說如果後來要實體化有實作 ILogService 的類別,那他就會實體化 AWSLogServcie 出來。

那 Service Provider 是什麼 ?

它就個註冊與管理 Container 內服務的地方。

下面的程式碼為 Laravel 專案的 Service Provider,其中有兩個重要的方法bootregister

  • register : 它就是用來寫 bind 的地方。
  • boot : 它就是當 register 結束以後會執行的方法。
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(
            PostRepositoryInterface::class,
            PostRepository::class
        );
    }
}

那什麼時後會需要使用 boot ?

boot 這個方法的執行時機為 register 執行完以後,那它什麼時後要用到他呢 ? 我覺得比較好的定義如下 :

在實際上使用這個 service 前,所需要做的前處理。

例如下面的 Laravel 官網授權章節所寫的範例,在要使用 AuthService 前它需要先將一些 policy 先註冊,這時就很適合寫在 boot 這個方法裡面。

Laravel-授權


<?php

namespace App\Providers;

use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * 註冊任何應用程式的認證或授權服務。
     *
     * @param  \Illuminate\Contracts\Auth\Access\Gate  $gate
     * @return void
     */
    public function boot(GateContract $gate)
    {
        $this->registerPolicies($gate);

        $gate->define('update-post', function ($user, $post) {
            return $user->id === $post->user_id;
        });
    }
}

或是


<?php
class DatabaseServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application events.
     *
     * @return void
     */
    public function boot()
    {
        Model::setConnectionResolver($this->app['db']);

        Model::setEventDispatcher($this->app['events']);
    }
    
    public function register()
    {
        Model::clearBootedModels();
        $this->registerConnectionServices();
        $this->registerEloquentFactory();
        $this->registerQueueableEntityResolver();
    }
}

為什麼要使用 Service Provider 呢 ?

上面我們大概理解了 Service Provider 以後,那接下來我們就來思考一件事情。

為什麼要使用 Service Provider 呢 ?

我們先假設沒有 Service Provider,然後來看看程式碼會變成什麼樣子。

假設我們已經產生了 container,就如下程式碼的 $app 。

<?php
public/index.php

$app = require_once __DIR__.'/../bootstrap/app.php';

$app->bind(IUserService:class, UserESrvice:class);
$app->bind(IMessageService:class, MessageService:class);

事實上現在這樣是還沒什麼問題,那如果是這樣呢 ?


<?php
public/index.php

$app = require_once __DIR__.'/../bootstrap/app.php';

$app->bind(IUserService:class, UserService:class);
$app->bind(IMessageService:class, MessageService:class);

$app->singleton('redis', function ($app) {
     $config = $app->make('config')->get('database.redis', []);
     return new RedisManager($app, Arr::pull($config, 'client', 'predis'), $config);
});

$app->bind('redis.connection', function ($app) {
     return $app['redis']->connection();
});

$app->singleton('cache', function ($app) {
     return new CacheManager($app);
});

$app->singleton('cache.store', function ($app) {
     return $app['cache']->driver();
});

 
$this->app->singleton('db', function ($app) {
     return new DatabaseManager($app, $app['db.factory']);
});
        
$this->app->singleton('db.factory', function ($app) {
     return new ConnectionFactory($app);
});

你可以發現這個檔案已經開發有點腫大,而且這樣在多人開發時,你會發現一直的 merge conflict 。

然後接下來,你可能在使用 db 前還需要設定一下東西,然後這個檔案變的如下。


<?php
public/index.php

$app = require_once __DIR__.'/../bootstrap/app.php';

$app->bind(IUserService:class, UserService:class);
$app->bind(IMessageService:class, MessageService:class);

$app->singleton('redis', function ($app) {
     $config = $app->make('config')->get('database.redis', []);
     return new RedisManager($app, Arr::pull($config, 'client', 'predis'), $config);
});

$app->bind('redis.connection', function ($app) {
     return $app['redis']->connection();
});

$app->singleton('cache', function ($app) {
     return new CacheManager($app);
});

$app->singleton('cache.store', function ($app) {
     return $app['cache']->driver();
});

$this->app->singleton('db', function ($app) {
     return new DatabaseManager($app, $app['db.factory']);
});
        
$this->app->singleton('db.factory', function ($app) {
     return new ConnectionFactory($app);
});

Model::setConnectionResolver($this->app['db']);
Model::setEventDispatcher($this->app['events']);

這時你就會很明顯的注意到有幾項缺點 :

  • 這個檔案太腫大了,每個人都需要修改到他。
  • 這個檔案做太多事情了,要產生 container、要註冊服務、註冊服務的前處理。

也就是因為這些原因,因此 Laravel 就產生了 Service Provider,並且它基本上是會根據模組來產生不同的 Provider,像以上面的範例就可以分拆成 db、cache、redis 等 provider。

某些方面這就違反了 SRP(Single Responsibility Principle)單一責任原則

雖然上述的原則是用在類別或方法上,但是我覺得以概念上來看,也可以用在上面這種狀況。

Laravel Service Provider 流程原始碼分析

接下來這章節我們要來理解一下 Laravel 是什麼時後註冊 Service provider,並且它內部是如何執行。

在 Laravel 中所有一切的源頭就是創建 Container,所以就從這裡開始看。

php artisan serve

首先產生完 containter ($app) 以後,接下來 Laravel 會在實體化 $kernel,它可以說是所有操作的核心模式,然後接下來 handler 它會執行所有 input 進來的東西,而這裡面就有用來處理 service provider 的地方。

#!/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);

Kernel 原始碼

這裡在執行 handler 裡面有個我們要注意的東西就是 bootstraps,這東東你可以想成它就是要完成一件事情所需要做的事情,其中我們要看的為 RegisterProviders 這個 bootstrap。


<?php
namespace Illuminate\Foundation\Console;

class Kernel implements KernelContract
{
   /**
     * The bootstrap classes for the application.
     *
     * @var array
     */
    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,
    ];

   /**
     * Run the console application.
     *
     * @param  \Symfony\Component\Console\Input\InputInterface  $input
     * @param  \Symfony\Component\Console\Output\OutputInterface  $output
     * @return int
     */
    public function handle($input, $output = null)
    {
        try {
            // 執行每個 boostrap
            $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 the application for artisan commands.
     *
     * @return void
     */
    public function bootstrap()
    {
        if (! $this->app->hasBeenBootstrapped()) {
            $this->app->bootstrapWith($this->bootstrappers());
        }

        $this->app->loadDeferredProviders();

        if (! $this->commandsLoaded) {
            $this->commands();

            $this->commandsLoaded = true;
        }
    }
}

RegisterProviders 原始碼

RegisterProviders 就是一個定義好的 boostrap 類別,它主要就是呼叫 container 來註冊 service provider。

<?php

namespace Illuminate\Foundation\Bootstrap;

use Illuminate\Contracts\Foundation\Application;

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

Container registerConfiguredProviders 原始碼

這一段原始碼中,laravel 會先將在 config/app.php 裡面有的 provider 先組合出包含 namespace 的 service provider 陣列,然後最在在丟給 ProviderRepository 的 load 方法來進行讀取。

<?php

    /**
     * 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\\');
                        });

        // 這裡看起來應該是去拿所有 packet 裡面的 provider
        $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);

        (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
                    ->load($providers->collapse()->toArray());
    }

ProviderRepository 原始碼

這一段程式碼就是實際執行 service proivder 裡面方法的地方,它的執行流程如下。

  1. 讀取 boostrap/cache/services.php。
  2. 判斷是否重新產生 cache 檔。
  3. 從 cache 檔中判斷每個 servcie provider 是那一類型,然後分別執行。

基本上 cache 檔中 service provider 被分為三種類別,它們的特點如下 :

  • when : 當某個事件被執行的時後,才會執行 service provider。
  • eager : 直接執行 service provider。
  • deferred : 等到要執行 make 前,才會執行 service provider。
<?php
   /**
     * Register the application service providers.
     *
     * @param  array  $providers
     * @return void
     */
    public function load(array $providers)
    {
        // 讀取 boostrap/cache/services.php
        $manifest = $this->loadManifest();

        // 產生 boostrap/cache/services.php
        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);
        }

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

備註

關於 boostrap/cache/services.php

這個檔案基本上組合如下圖,共有分為四個部份,它們的功用如下。

  • provider : 包含所有 config/app.php 裡有註冊的 service providers,基本上它的功用就是用來判斷 cache 檔案要不要進行修。
  • eager : service provider 類型,它會在 load 時執行 service provider。
  • deferred : service provider 類型,它會在 make 時執行 service provider。
  • when : service provider 類型,它會在收到某個事件時執行 service provider。

將 servcie provider 修改為 deferred 時記得砍 cache

Laravel 在判斷直接使用 cache 時有兩個必要條件 :

  • 有 cache 檔。
  • cache 檔的 providers 於 config/app.php 的 providers 是相同的。

所以如果你這時修改了某個 provider 的 deferred 時,依然符合上述兩個條件,因為你只是改變 proivder 的屬性而不是名稱,因此還會繼續使用 cache 檔。

所以這時記得要砍掉 cache 檔讓它進行重建。

參考資料

comments powered by Disqus