Laravel核心程式碼學習 -- 服務提供器

kevinyan發表於2018-05-22

服務提供器(ServiceProvider)

服務提供器是所有 Laravel 應用程式引導中心。你的應用程式自定義的服務、第三方資源包提供的服務以及 Laravel 的所有核心服務都是通過服務提供器進行註冊(register)和引導(boot)的。

拿一個Laravel框架自帶的服務提供器來舉例子

class BroadcastServiceProvider extends ServiceProvider
{
    protected $defer = true;
    public function register()
    {
        $this->app->singleton(BroadcastManager::class, function ($app) {
            return new BroadcastManager($app);
        });
        $this->app->singleton(BroadcasterContract::class, function ($app) {
            return $app->make(BroadcastManager::class)->connection();
        });
        //將BroadcastingFactory::class設定為BroadcastManager::class的別名
        $this->app->alias(
            BroadcastManager::class, BroadcastingFactory::class
        );
    }
    public function provides()
    {
        return [
            BroadcastManager::class,
            BroadcastingFactory::class,
            BroadcasterContract::class,
        ];
    }
}
複製程式碼

在服務提供器BroadcastServiceProviderregister中, 為BroadcastingFactory的類名繫結了類實現BroadcastManager,這樣就能通過服務容器來make出通過BroadcastingFactory::class繫結的服務BroadcastManger物件供應用程式使用了。

本文主要時來梳理一下laravel是如何註冊、和初始化這些服務的,關於如何編寫自己的服務提供器,可以參考官方文件

BootStrap

首先laravel註冊和引導應用需要的服務是發生在尋找路由處理客戶端請求之前的Bootstrap階段的,在框架的入口檔案裡我們可以看到,框架在例項化了Application物件後從服務容器中解析出了HTTP Kernel物件

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

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);
複製程式碼

在Kernel處理請求時會先讓請求通過中介軟體然後在傳送請求給路由對應的控制器方法, 在這之前有一個BootStrap階段來引導啟動Laravel應用程式,如下面程式碼所示。

public function handle($request)
{
	......
    $response = $this->sendRequestThroughRouter($request);
    ......
            
    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());
}
    
//引導啟動Laravel應用程式
public function bootstrap()
{
    if (! $this->app->hasBeenBootstrapped()) {
	    /**依次執行$bootstrappers中每一個bootstrapper的bootstrap()函式
         $bootstrappers = [
             'Illuminate\Foundation\Bootstrap\DetectEnvironment',
             'Illuminate\Foundation\Bootstrap\LoadConfiguration',
             'Illuminate\Foundation\Bootstrap\ConfigureLogging',
             'Illuminate\Foundation\Bootstrap\HandleExceptions',
             'Illuminate\Foundation\Bootstrap\RegisterFacades',
             'Illuminate\Foundation\Bootstrap\RegisterProviders',
             'Illuminate\Foundation\Bootstrap\BootProviders',
            ];*/
        $this->app->bootstrapWith($this->bootstrappers());
    }
}
複製程式碼

上面bootstrap中會分別執行每一個bootstrapper的bootstrap方法來引導啟動應用程式的各個部分

    1. DetectEnvironment  檢查環境
    2. LoadConfiguration  載入應用配置
    3. ConfigureLogging   配置日至
    4. HandleException    註冊異常處理的Handler
    5. RegisterFacades    註冊Facades 
    6. RegisterProviders  註冊Providers 
    7. BootProviders      啟動Providers
複製程式碼

啟動應用程式的最後兩部就是註冊服務提供這和啟動提供者,如果對前面幾個階段具體時怎麼實現的可以參考這篇文章。在這裡我們主要關注服務提供器的註冊和啟動。

先來看註冊服務提供器,服務提供器的註冊由類 \Illuminate\Foundation\Bootstrap\RegisterProviders::class 負責,該類用於載入所有服務提供器的 register 函式,並儲存延遲載入的服務的資訊,以便實現延遲載入。

class RegisterProviders
{
    public function bootstrap(Application $app)
    {
        //呼叫了Application的registerConfiguredProviders()
        $app->registerConfiguredProviders();
    }
}
    
class Application extends Container implements ApplicationContract, HttpKernelInterface
{
    public function registerConfiguredProviders()
    {
        (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
                    ->load($this->config['app.providers']);
    }
    
    public function getCachedServicesPath()
    {
        return $this->bootstrapPath().'/cache/services.php';
    }
}
複製程式碼

可以看出,所有服務提供器都在配置檔案 app.php 檔案的 providers 陣列中。類 ProviderRepository 負責所有的服務載入功能:

class ProviderRepository
{
      public function load(array $providers)
    {
        $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);
        }
        $this->app->addDeferredServices($manifest['deferred']);
    }
}
複製程式碼

loadManifest()會載入服務提供器快取檔案services.php,如果框架是第一次啟動時沒有這個檔案的,或者是快取檔案中的providers陣列項與config/app.php裡的providers陣列項不一致都會編譯生成services.php。

//判斷是否需要編譯生成services檔案
public function shouldRecompile($manifest, $providers)
{
    return is_null($manifest) || $manifest['providers'] != $providers;
}

//編譯生成檔案的具體過程
protected function compileManifest($providers)
{
    $manifest = $this->freshManifest($providers);
    foreach ($providers as $provider) {
        $instance = $this->createProvider($provider);
        if ($instance->isDeferred()) {
            foreach ($instance->provides() as $service) {
                $manifest['deferred'][$service] = $provider;
            }
            $manifest['when'][$provider] = $instance->when();
        }
        else {
            $manifest['eager'][] = $provider;
        }
    }
    return $this->writeManifest($manifest);
}


protected function freshManifest(array $providers)
{
    return ['providers' => $providers, 'eager' => [], 'deferred' => []];
}
複製程式碼
  • 快取檔案中 providers 放入了所有自定義和框架核心的服務。
  • 如果服務提供器是需要立即註冊的,那麼將會放入快取檔案中 eager 陣列中。
  • 如果服務提供器是延遲載入的,那麼其函式 provides() 通常會提供服務別名,這個服務別名通常是向服務容器中註冊的別名,別名將會放入快取檔案的 deferred 陣列中,與真正要註冊的服務提供器組成一個鍵值對。
  • 延遲載入如果由 event 事件啟用,那麼可以在 when 函式中寫入事件類,並寫入快取檔案的 when 陣列中。

生成的快取檔案內容如下:

array (
    'providers' => 
    array (
      0 => 'Illuminate\\Auth\\AuthServiceProvider',
      1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
      ...
    ),

	'eager' => 
	array (
      0 => 'Illuminate\\Auth\\AuthServiceProvider',
      1 => 'Illuminate\\Cookie\\CookieServiceProvider',
      ...
    ),

    'deferred' => 
    array (
      'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
      'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
      ...
    ),

    'when' => 
    array (
      'Illuminate\\Broadcasting\\BroadcastServiceProvider' => 
      array (
      ),
      ...
)
複製程式碼

事件觸發時註冊延遲服務提供器

延遲服務提供器除了利用 IOC 容器解析服務方式啟用,還可以利用 Event 事件來啟用:

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方法由Application的register方法裡來呼叫:

class Application extends Container implements ApplicationContract, HttpKernelInterface
{
    public function register($provider, $options = [], $force = false)
    {
        if (($registered = $this->getProvider($provider)) && ! $force) {
            return $registered;
        }
        if (is_string($provider)) {
            $provider = $this->resolveProvider($provider);
        }
        if (method_exists($provider, 'register')) {
            $provider->register();
        }
        $this->markAsRegistered($provider);
        if ($this->booted) {
            $this->bootProvider($provider);
        }
        return $provider;
    }

    public function getProvider($provider)
    {
        $name = is_string($provider) ? $provider : get_class($provider);
        return Arr::first($this->serviceProviders, function ($value) use ($name) {
	        return $value instanceof $name;
    	});
	}

	public function resolveProvider($provider)
	{
    	return new $provider($this);
	}

    protected function markAsRegistered($provider)
	{
		//這個屬性在稍後booting服務時會用到
    	$this->serviceProviders[] = $provider;
        $this->loadedProviders[get_class($provider)] = true;
    }

    protected function bootProvider(ServiceProvider $provider)
    {
        if (method_exists($provider, 'boot')) {
        	return $this->call([$provider, 'boot']);
    	}
     }
}
複製程式碼

可以看出,服務提供器的註冊過程:

  • 判斷當前服務提供器是否被註冊過,如註冊過直接返回物件
  • 解析服務提供器
  • 呼叫服務提供器的 register 函式
  • 標記當前服務提供器已經註冊完畢
  • 若框架已經載入註冊完畢所有的服務容器,那麼就啟動服務提供器的 boot 函式,該函式由於是 call 呼叫,所以支援依賴注入。

服務解析時註冊延遲服務提供器

延遲服務提供器首先需要新增到 Application 中

public function addDeferredServices(array $services)
{
	$this->deferredServices = array_merge($this->deferredServices, $services);
}
複製程式碼

我們之前說過,延遲服務提供器的啟用註冊有兩種方法:事件與服務解析。

當特定的事件被激發後,就會呼叫 Application 的 register 函式,進而呼叫服務提供器的 register 函式,實現服務的註冊。

當利用 Ioc 容器解析服務名時,例如解析服務名 BroadcastingFactory:

class BroadcastServiceProvider extends ServiceProvider
{
    protected $defer = true;
	
    public function provides()
    {
        return [
            BroadcastManager::class,
            BroadcastingFactory::class,
            BroadcasterContract::class,
        ];
    }
}
複製程式碼

在Application的make方法裡會通過別名BroadcastingFactory查詢是否有對應的延遲註冊的服務提供器,如果有的話那麼 就先通過registerDeferredProvider方法註冊服務提供器。

class Application extends Container implements ApplicationContract, HttpKernelInterface
{
    public function make($abstract)
    {
        $abstract = $this->getAlias($abstract);
	      if (isset($this->deferredServices[$abstract])) {
     	      $this->loadDeferredProvider($abstract);
    	  }
    	  return parent::make($abstract);
	  }

    public function loadDeferredProvider($service)
    {
        if (! isset($this->deferredServices[$service])) {
            return;
    	}
      	$provider = $this->deferredServices[$service];
      	if (! isset($this->loadedProviders[$provider])) {
          	$this->registerDeferredProvider($provider, $service);
        }
    }
}
複製程式碼

由 deferredServices 陣列可以得知,BroadcastingFactory 為延遲服務,接著程式會利用函式 loadDeferredProvider 來載入延遲服務提供器,呼叫服務提供器的 register 函式,若當前的框架還未註冊完全部服務。那麼將會放入服務啟動的回撥函式中,以待服務啟動時呼叫:

public function registerDeferredProvider($provider, $service = null)
{
    if ($service) {
        unset($this->deferredServices[$service]);
    }
    $this->register($instance = new $provider($this));
    if (! $this->booted) {
        $this->booting(function () use ($instance) {
            $this->bootProvider($instance);
        });
    }
}
複製程式碼

還是拿服務提供器BroadcastServiceProvider來舉例:

class BroadcastServiceProvider extends ServiceProvider
{
    protected $defer = true;
    public function register()
    {
        $this->app->singleton(BroadcastManager::class, function ($app) {
            return new BroadcastManager($app);
        });
        $this->app->singleton(BroadcasterContract::class, function ($app) {
            return $app->make(BroadcastManager::class)->connection();
        });
        //將BroadcastingFactory::class設定為BroadcastManager::class的別名
        $this->app->alias(
            BroadcastManager::class, BroadcastingFactory::class
        );
    }
    public function provides()
    {
        return [
            BroadcastManager::class,
            BroadcastingFactory::class,
            BroadcasterContract::class,
        ];
    }
}
複製程式碼

函式 register 為類 BroadcastingFactory服務容器繫結了特定的實現類 BroadcastManagerApplication中的 make 函式裡執行parent::make($abstract) 通過服務容器的make就會正確的解析出服務 BroadcastingFactory

因此函式 provides() 返回的元素一定都是 register()服務容器中繫結的類名或者別名。這樣當我們利用App::make() 解析這些類名的時候,服務容器才會根據服務提供器的 register() 函式中繫結的實現類,正確解析出服務功能。

啟動Application

Application的啟動由類 \Illuminate\Foundation\Bootstrap\BootProviders 負責:

class BootProviders
{
    public function bootstrap(Application $app)
    {
        $app->boot();
    }
}
class Application extends Container implements ApplicationContract, HttpKernelInterface
{
	public function boot()
    {
        if ($this->booted) {
            return;
        }
        $this->fireAppCallbacks($this->bootingCallbacks);
        array_walk($this->serviceProviders, function ($p) {
            $this->bootProvider($p);
        });
        $this->booted = true;
        $this->fireAppCallbacks($this->bootedCallbacks);
    }
    
    protected function bootProvider(ServiceProvider $provider)
    {
        if (method_exists($provider, 'boot')) {
            return $this->call([$provider, 'boot']);
        }
    }
}
複製程式碼

引導應用Application的serviceProviders屬性中記錄的所有服務提供器,就是依次呼叫這些服務提供器的boot方法,引導完成後$this->booted = true 就代表應用Application正式啟動了,可以開始處理請求了。這裡額外說一句,之所以等到所有服務提供器都註冊完後再來進行引導是因為有可能在一個服務提供器的boot方法裡呼叫了其他服務提供器註冊的服務,所以需要等到所有即時註冊的服務提供器都register完成後再來boot。

本文已經收錄在系列文章Laravel原始碼學習裡,歡迎訪問閱讀。

相關文章