什麼是 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,其中有兩個重要的方法boot
與register
。
- 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 這個方法裡面。
<?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 裡面方法的地方,它的執行流程如下。
- 讀取 boostrap/cache/services.php。
- 判斷是否重新產生 cache 檔。
- 從 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 檔讓它進行重建。