深入剖析 Laravel 服務容器

liuqing_hu發表於2018-05-29

本文首發於 深入剖析 Laravel 服務容器,轉載請註明出處。喜歡的朋友不要吝嗇你們的贊同,謝謝。

之前在 深度挖掘 Laravel 生命週期 一文中,我們有去探究 Laravel 究竟是如何接收 HTTP 請求,又是如何生成響應並最終呈現給使用者的工作原理。

本章將帶領大家研究另一個 Laravel 框架的核心內容:「服務容器」。有閱讀過 Laravel 文件 的朋友應該有注意到在「核心架構」篇章中包含了幾個主題:生命週期服務容器服務提供者FacadesConcracts.

今天就讓我們一起來揭開「Laravel 服務容器」的神祕面紗。

提示:本文內容較長可能需要耗費較多的閱讀時間,另外文中包含 Laravel 核心程式碼建議選擇合適的 IDE 或文字編輯器進行原始碼閱讀。

目錄結構

  • 序章
  • 依賴注入基本概念
    • 什麼是依賴注入
    • 什麼是依賴注入容器
    • 什麼是控制反轉(IoC)
  • Laravel 服務容器是什麼
    • 小結
  • Laravel 服務容器的使用方法
    • 管理待建立類的依賴
    • 常用繫結方法
      • bind 簡單繫結
      • singleton 單例繫結
      • instance 例項繫結
      • contextual-binding 上下文繫結
      • 自動注入和解析
  • Laravel 服務容器實現原理
    • 註冊基礎服務
      • 註冊基礎服務提供者
      • 註冊核心服務別名到容器
    • 管理所需建立的類及其依賴
      • bind 方法執行原理
      • make 解析處理
  • 資料

序章

如果您有閱讀我的前作 深度挖掘 Laravel 生命週期 一文,你應該已經注意到「APP 容器」、「服務容器」、「繫結」和「解析」這些字眼。沒錯這些技術都和「Laravel 服務容器」有著緊密的聯絡。

在學習什麼是「Laravel 服務容器」之前,如果您對「IoC(控制反轉)」、「DI(依賴注入)」和「依賴注入容器」等相關知識還不夠了解的話,建議先學習一下這些資料:

雖然,這些學習資料都有細緻的講解容器相關的概念。但介紹一下與「Laravel 服務容器」有關的基本概念仍然有必要。

依賴注入基本概念

這個小節會捎帶講解下「IoC(控制反轉)」、「DI(依賴注入)」和「依賴注入容器」這些概念。

什麼是依賴注入

應用程式對需要使用的依賴「外掛」在編譯(編碼)階段僅依賴於介面的定義,到執行階段由一個獨立的組裝模組(容器)完成對實現類的例項化工作,並將其「注射」到應用程式中稱之為「依賴注入」。

一言以蔽之:面向介面程式設計。

至於如何實現面向介面程式設計,在 依賴注入系列教程 的前兩篇中有例項演示,感興趣的朋友可以去閱讀這個教程。更多細節可以閱讀 Inversion of Control Containers and the Dependency Injection pattern深入淺出依賴注入

什麼是依賴注入容器

在依賴注入過程中,由一個獨立的組裝模組(容器)完成對實現類的例項化工作,那麼這個組裝模組就是「依賴注入容器」。

通俗一點講,使用「依賴注入容器」時無需人肉使用 new 關鍵字去例項化所依賴的「外掛」,轉而由「依賴注入容器」自動的完成一個模組的組裝、配置、例項化等工作。

什麼是控制反轉(IoC)

IoC 是 Inversion of Control 的簡寫,通常被稱為控制反轉,控制反轉從字面上來說比較不容易被理解。

要掌握什麼是「控制反轉」需要整明白專案中「控制反轉」究竟「反轉」了哪方面的「控制」,它需要解決如何去定位(獲取)服務所需要的依賴的實現。

實現控制反轉時,通過將原先在模組內部完成具體實現類的例項化,移至模組的外部,然後再通過「依賴注入」的方式將具體例項「注入」到模組內即完成了對控制的反轉操作。

「依賴注入」的結果就是「控制反轉」的目的,也就說 控制反轉 的最終目標是為了 實現專案的高內聚低耦合,而 實現這種目標 的方式則是通過 依賴注入 這種設計模式。

以上就是一些有關服務容器的一些基本概念。和我前面說的一樣,本文不是一篇講解依賴注入的文章,所以更多的細節需要大家自行去學習我之前列出的參考資料。

接下來才是今天的正餐,我將從以下幾個角度講解 Laravel 服務容器的相關內容:

  • Laravel 服務容器是什麼;
  • Laravel 服務容器的使用方法;
  • Laravel 服務容器技術原理。

Laravel 服務容器是什麼

Laravel 文件 中,有一段關於 Laravel 服務容器的介紹:

Laravel 服務容器是用於管理類的依賴和執行依賴注入的工具。依賴注入這個花俏名詞實質上是指:類的依賴項通過建構函式,或者某些情況下通過「setter」方法「注入」到類中。

劃下重點,「Laravel 服務容器」是用於 管理類的依賴執行依賴注入工具

通過前一節「依賴注入基本概念」相關闡述,我們不難得出這樣一個簡單的結論「Laravel 服務容器」就是「依賴注入容器」。

其實,服務容器作為「依賴注入容器」去完成 Laravel 所需依賴的註冊、繫結和解析工作只是 「Laravel 服務容器」核心功能之一;另外,「Laravel 服務容器」還擔綱 Laravel 應用的註冊程式的功能。

節選一段「深度挖掘 Laravel 生命週期」一文中有關服務容器的內容:

建立應用例項即例項化 Illuminate\Foundation\Application 這個服務容器,後續我們稱其為 APP 容器。在建立 APP 容器主要會完成:註冊應用的基礎路徑並將路徑繫結到 APP 容器 、註冊基礎服務提供者至 APP 容器 、註冊核心容器別名至 APP 容器 等基礎服務的註冊工作。

所以要了解 Larvel 服務容器必然需要研究 Illuminate\Foundation\Application 的建構函式:

    /**
     * Create a new Illuminate application instance.
     *
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Application.php#L162:27
     * @param  string|null  $basePath
     * @return void
     */
    public function __construct($basePath = null)
    {
        if ($basePath) {
            $this->setBasePath($basePath);
        }
        $this->registerBaseBindings();
        $this->registerBaseServiceProviders();
        $this->registerCoreContainerAliases();
    }

沒錯在 Application 類的建構函式一共完成 3 個操作的處理功能:

  • 通過 registerBaseBindings() 方法將「App 例項(即 Laravel 服務容器)」自身註冊到「Laravel 服務容器」;
  • 通過 registerBaseServiceProviders() 註冊應用 Laravel 框架的基礎服務提供者;
  • 通過 registerCoreContainerAliases() 將具體的「依賴注入容器」及其別名註冊到「Laravel 服務容器」。

這裡所說的「註冊」歸根到底還是在執行「Laravel 服務容器」的「繫結(bind)」操作,完成繫結介面到實現。

為了表名我所言非虛,讓我們看看 registerBaseBindings() 方法:

    /**
     * Register the basic bindings into the container. 註冊 App 例項本身到 App 容器
     *
     * @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()
        ));
    }

我們知道 instance() 方法會將物件例項 $this 繫結到容器的 appContainer::class 介面。後續無論是通過 app()->make('app') 還是 app()->make(Container::class) 獲取到的實現類都是 $this(即 Laravel 服務容器例項) 物件。有關 instance 的使用方法可以查閱 Laravel 服務容器解析文件,不過我也會在下文中給出相關使用說明。

到這裡相信大家對「Laravel 服務容器」有了一個比較清晰的理解了。

小結

我們所說的「Laravel 服務容器」除了擔綱「依賴注入容器」職能外;同時,還會作為 Laravel 專案的註冊中心去完成基礎服務的註冊工作。直白一點講在它的內部會將諸多服務的實現類「繫結」到「Laravel 服務容器」。總結起來它的作用主要可以歸為以下 2 方面:

  1. 註冊基礎服務;
  2. 管理所需建立的類及其依賴。

Laravel 服務容器的使用方法

Laravel 服務容器在使用時一般分為兩個階段:使用之前進行繫結(bind)完成將實現繫結到介面;使用時對通過介面解析(make)出服務。

Laravel 內建多種不同的繫結方法以用於不同的使用場景。但無論哪種繫結方式,它們的最終目標是一致的:繫結介面到實現。

這樣的好處是在專案的編碼階段建立起介面和實現的對映關係,到使用階段通過抽象類(介面)解析出它的具體實現,這樣就實現了專案中的解耦。

在講解這些繫結方法前,先講一個 Laravel 服務容器的使用場景。

管理待建立類的依賴

通過向服務容器中繫結需要建立的類及其依賴,當需要使用這個類時直接從服務容器中解析出這個類的例項。類的例項化及其依賴的注入,完全由服務容器自動的去完成。

舉個示例,相比於通過 new 關鍵詞建立類例項:

<?php
$dependency = new ConfigDependency(config('cache.config.setting'));
$cache = new MemcachedCache($dependency);

每次例項化時我們都需要手動的將依賴 $dependency 傳入到建構函式內。

而如果我們通過「Laravel 服務容器」繫結來管理依賴的話:

<?php
App::bind(Cache::class, function () {
    $dependency = new ConfigDependency(config('cache.config.setting'));
    return $cache = new MemcachedCache($dependency);
});

僅需在匿名函式內一次建立所需依賴 $dependency,再將依賴傳入到服務進行例項化,並返回服務例項。

此時,使用 Cache 服務時只要從「Laravel 服務容器」中解析(make)出來即可,而無需每次手動傳入 ConfigDependency 依賴再例項化服務。因為,所有的依賴注入工作此時都由 Laravel 服務容器 自動的給我們做好了,這樣就簡化了服務處理。

下面演示瞭如何解析出 Cache 服務:

<?php
$cache = App::make(Cache::class);

先了解 Laravel 服務容器的一個使用場景,會對學習服務容器的 繫結方式 大有裨益。

Laravel 服務容器解析 - 繫結 這部分的文件我們知道常用的繫結方式有:

  • bind($abstract, $concrete) 簡單繫結:將實現繫結到介面,解析時每次返回新的例項;
  • singleton($abstract, $concrete) 單例繫結:將實現繫結到介面,與 bind 方法不同的是首次解析是建立例項,後續解析時直接獲取首次解析的例項物件;
  • instance($abstract, $instance) 例項繫結:將實現例項繫結到介面;
  • 上下文繫結和自動注入。

接下來我們將學習這些繫結方法。

常用繫結方法

bind 簡單繫結

bind 方法的功能是將服務的實現繫結到抽象類,然後在每次執行服務解析操作時,Laravel 容器都會重新建立例項物件。

bind 的使用方法已經在「管理待建立類的依賴」一節中有過簡單的演示,它會在每次使用 App::make(Cache::class) 去解析 Cache 服務時,重新執行「繫結」操作中定義的閉包而重新建立 MemcachedCache 快取例項。

bind 方法除了能夠接收閉包作為實現外,還可以:

  • 接收具體實現類的類名;
  • 接收 null 值以繫結自身。

singleton 單例繫結

採用單例繫結時,僅在首次解析時建立例項,後續使用 make 進行解析服務操作都將直接獲取這個已解析的物件,實現了 共享 操作。

繫結處理類似 bind 繫結,只需將 bind 方法替換成 singleton 方法即可:

App::singleton(Cache::class, function () {
    $dependency = new ConfigDependency(config('cache.config.setting'));
    return $cache = new MemcachedCache($dependency);
});

instance 例項繫結

例項繫結的功能是將已經建立的例項物件繫結到介面以供後續使用,這種使用場景類似於 登錄檔

比如用於儲存使用者模型:

<?php
// 建立一個使用者例項
$artisan = new User('柳公子');

// 將例項繫結到服務容器
App::instance('login-user', $artisan);

// 獲取使用者例項
$artisan = App::make('login-user');

contextual-binding 上下文繫結

在瞭解上下文繫結之前,先解釋下什麼是上下文,引用「輪子哥」的一段解釋:

每一段程式都有很多外部變數。只有像Add這種簡單的函式才是沒有外部變數的。一旦你的一段程式有了外部變數,這段程式就不完整,不能獨立執行。你為了使他們執行,就要給所有的外部變數一個一個寫一些值進去。這些值的集合就叫上下文。 「程式設計中什麼是「Context(上下文)」?」 - vczh的回答

上下文繫結在 Laravel 服務容器解析 - 上下文繫結 文件中給出了相關示例:

use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\PhotoController;
use App\Http\Controllers\VideoController;
use Illuminate\Contracts\Filesystem\Filesystem;

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('local');
          });

$this->app->when(VideoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('s3');
          });

在專案中常會用到儲存功能,得益於 Laravel 內建整合了 FlySystemFilesystem 介面,我們很容易實現多種儲存服務的專案。

示例中將使用者頭像儲存到本地,將使用者上傳的小視訊儲存到雲服務。那麼這個時就需要區分這樣不同的使用場景(即上下文或者說環境)。

當使用者儲存頭像(PhotoController::class)需要使用儲存服務(Filesystem::class)時,我們將本地儲存驅動,作為實現給到 PhotoController::class

function () {
    return Storage::disk('local');
}

而當使用者上傳視訊 VideoController::class,需要使用儲存服務(Filesystem::class)時,我們則將雲服務驅動,作為實現給到 VideoController::class

function () {
    return Storage::disk('s3');
}

這樣就實現了基於不同的環境獲取不同的服務實現。

自動注入和解析

「Laravel 服務容器」功能強大的原因在於除了提供手動的繫結介面到實現的方法,還支援自動注入和解析的功能。

我們在編寫控制器時,經常會使用型別提示功能將某個類作為依賴傳入建構函式;但在執行這個類時卻無需我們去例項化這個類所需的依賴,這一切歸功於自動解析的能力。

比如,我們的使用者控制器需要獲取使用者資訊,然後在建構函式中定義 User 模型作為依賴:

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;
class UserController
{
    private $user = null;

    public function __construct(User $user)
    {
        $this->user = $user;
    }
}

然後,當訪問使用者模組時 Laravel 會自動解析出 User 模型,而無需手動的常見模型示例。

除了以上幾種資料繫結方法外還有 tag(標籤繫結)extend(擴充套件繫結) 等,毫無疑問這些內容在 Laravel 文件 也有介紹,所以這裡就不再過多介紹了。

下一節,我們將深入到原始碼中去窺探下 Laravel 服務容器是如何進行繫結和解析處理的。

Laravel 服務容器實現原理

要了解一項技術的實現原理,免不了去探索原始碼,原始碼學習是個有意思的事情。這個過程不但讓我們理解它是如何工作的,或許還會帶給我們一些意外驚喜。

我們知道 Laravel 服務容器其實會處理以下兩方面的工作:

  1. 註冊基礎服務;
  2. 管理所需建立的類及其依賴。

註冊基礎服務

關於註冊基礎服務,在「深度挖掘 Laravel 生命週期」一文中其實已經有所涉及,但並並不深入。

本文將進一步的研究註冊基礎服務的細節。除了研究這些服務究竟如何被註冊到服務容器,還將學習它們是如何被使用的。所有的這些都需要我們深入到 Illuminate\Foundation\Application 類的內部:

    /**
     * Create a new Illuminate application instance.
     *
     * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Application.php#L162:27
     * @param  string|null  $basePath
     * @return void
     */
    public function __construct($basePath = null)
    {
        if ($basePath) {
            $this->setBasePath($basePath);
        }
        $this->registerBaseBindings();
        $this->registerBaseServiceProviders();
        $this->registerCoreContainerAliases();
    }

前面我們已經研究過 registerBaseBindings() 方法,瞭解到該方法主要是將自身繫結到了服務容器,如此我們便可以在專案中使用 $this->app->make('something') 去解析一項服務。

現在讓我們將焦點集中到 registerBaseServiceProvidersregisterCoreContainerAliases 這兩個方法。

註冊基礎服務提供者

開啟 registerBaseServiceProviders 方法將發現在方法體中僅有 3 行程式碼,分別是註冊 EventServiceProviderLogServiceProviderRoutingServiceProvider 這 3 個服務提供者:


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

    /**
     * Register a service provider with the application.
     *
     * @param  \Illuminate\Support\ServiceProvider|string  $provider
     * @param  array  $options
     * @param  bool   $force
     * @return \Illuminate\Support\ServiceProvider
     */
    public function register($provider, $options = [], $force = false)
    {
        if (($registered = $this->getProvider($provider)) && ! $force) {
            return $registered;
        }

        // If the given "provider" is a string, we will resolve it, passing in the
        // application instance automatically for the developer. This is simply
        // a more convenient way of specifying your service provider classes.
        if (is_string($provider)) {
            $provider = $this->resolveProvider($provider);
        }

        // 當服務提供者存在 register 方法時,執行 register 方法,完成繫結處理
        if (method_exists($provider, 'register')) {
            $provider->register();
        }

        $this->markAsRegistered($provider);

        // If the application has already booted, we will call this boot method on
        // the provider class so it has an opportunity to do its boot logic and
        // will be ready for any usage by this developer's application logic.
        // 執行服務提供者 boot 方法啟動程式
        if ($this->booted) {
            $this->bootProvider($provider);
        }

        return $provider;
    }

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

Laravel 服務容器在執行註冊方法時,需要進行如下處理:

  1. 如果服務提供者存在 register 方法,會將服務實現繫結到容器操作 $provider->register();;
  2. 如果服務提供者存在 boot 方法,會在 bootProvider 方法內執行啟動方法來啟動這個服務。

值得指出的是在服務提供者的 register 方法中,最好僅執行「繫結」操作。

為了更好的說明服務提供者僅完成繫結操作,還是讓我們來瞧瞧 EventServiceProvider 服務,看看它究竟做了什麼:

<?php

namespace Illuminate\Events;

use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Queue\Factory as QueueFactoryContract;

class EventServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider. 註冊服務提供者
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('events', function ($app) {
            return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
                return $app->make(QueueFactoryContract::class);
            });
        });
    }
}

沒錯 EventServiceProvider 所做的全部事情,僅僅通過 register 方法將閉包繫結到了服務容器,除此之外就什麼都沒有了。

註冊核心服務別名到容器

用過 Laravel 框架的朋友應該知道在 Laravel 中有個別名系統。最常見的使用場景就是設定路由時,可以通過 Route 類完成一個新路由的註冊,如:

Route::get('/', function() {
    return 'Hello World';
});

得益於 Laravel Facades 和別名系統我們可以很方便的通過別名來使用 Laravel 內建提供的各種服務。

註冊別名和對應服務的對映關係,便是在 registerCoreContainerAliases 方法內來完成的。由於篇幅所限本文就不做具體細節的展開,後續會單獨出一篇講解別名系統的文章。

不過現在還是有必要瀏覽下 Laravel 提供了哪些別名服務:

    /**
     * Register the core class aliases in the container. 在容器中註冊核心服務的別名
     *
     * @return void
     */
    public function registerCoreContainerAliases()
    {
        foreach ([
            'app'                  => [\Illuminate\Foundation\Application::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class,  \Psr\Container\ContainerInterface::class],
            'auth'                 => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class],
            'auth.driver'          => [\Illuminate\Contracts\Auth\Guard::class],
            'blade.compiler'       => [\Illuminate\View\Compilers\BladeCompiler::class],
            'cache'                => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class],
            'cache.store'          => [\Illuminate\Cache\Repository::class, \Illuminate\Contracts\Cache\Repository::class],
            'config'               => [\Illuminate\Config\Repository::class, \Illuminate\Contracts\Config\Repository::class],
            'cookie'               => [\Illuminate\Cookie\CookieJar::class, \Illuminate\Contracts\Cookie\Factory::class, \Illuminate\Contracts\Cookie\QueueingFactory::class],
            'encrypter'            => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class],
            'db'                   => [\Illuminate\Database\DatabaseManager::class],
            'db.connection'        => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class],
            'events'               => [\Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class],
            'files'                => [\Illuminate\Filesystem\Filesystem::class],
            'filesystem'           => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class],
            'filesystem.disk'      => [\Illuminate\Contracts\Filesystem\Filesystem::class],
            'filesystem.cloud'     => [\Illuminate\Contracts\Filesystem\Cloud::class],
            'hash'                 => [\Illuminate\Contracts\Hashing\Hasher::class],
            'translator'           => [\Illuminate\Translation\Translator::class, \Illuminate\Contracts\Translation\Translator::class],
            'log'                  => [\Illuminate\Log\Writer::class, \Illuminate\Contracts\Logging\Log::class, \Psr\Log\LoggerInterface::class],
            'mailer'               => [\Illuminate\Mail\Mailer::class, \Illuminate\Contracts\Mail\Mailer::class, \Illuminate\Contracts\Mail\MailQueue::class],
            'auth.password'        => [\Illuminate\Auth\Passwords\PasswordBrokerManager::class, \Illuminate\Contracts\Auth\PasswordBrokerFactory::class],
            'auth.password.broker' => [\Illuminate\Auth\Passwords\PasswordBroker::class, \Illuminate\Contracts\Auth\PasswordBroker::class],
            'queue'                => [\Illuminate\Queue\QueueManager::class, \Illuminate\Contracts\Queue\Factory::class, \Illuminate\Contracts\Queue\Monitor::class],
            'queue.connection'     => [\Illuminate\Contracts\Queue\Queue::class],
            'queue.failer'         => [\Illuminate\Queue\Failed\FailedJobProviderInterface::class],
            'redirect'             => [\Illuminate\Routing\Redirector::class],
            'redis'                => [\Illuminate\Redis\RedisManager::class, \Illuminate\Contracts\Redis\Factory::class],
            'request'              => [\Illuminate\Http\Request::class, \Symfony\Component\HttpFoundation\Request::class],
            'router'               => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class],
            'session'              => [\Illuminate\Session\SessionManager::class],
            'session.store'        => [\Illuminate\Session\Store::class, \Illuminate\Contracts\Session\Session::class],
            'url'                  => [\Illuminate\Routing\UrlGenerator::class, \Illuminate\Contracts\Routing\UrlGenerator::class],
            'validator'            => [\Illuminate\Validation\Factory::class, \Illuminate\Contracts\Validation\Factory::class],
            'view'                 => [\Illuminate\View\Factory::class, \Illuminate\Contracts\View\Factory::class],
        ] as $key => $aliases) {
            foreach ($aliases as $alias) {
                $this->alias($key, $alias);
            }
        }
    }

管理所需建立的類及其依賴

對於 Laravel 服務容器來講,其內部實現上無論是 bindsingletontag 還是 extend 它們的基本原理大致類似。所以本文中我們僅研究 bind 繫結來管中窺豹。

我們知道繫結方法定義在 Laravel 服務容器 Illuminate\Foundation\Application 類內,而 Application繼承自 Illuminate\Container\Container 類。這些與服務容器繫結相關的方法便直接繼承自 Container 類。

bind 方法執行原理

bind 繫結作為最基本的繫結方法,可以很好的說明 Laravel 是如何實現繫結服務處理的。

下面摘出 Container 容器中 bind 方法及其相關聯的方法。由於繫結處理中涉及較多方法,所以我直接將重要的程式碼片段相關注釋做了翻譯及補充說明,以便閱讀:

    /**
     * Register a binding with the container.
     *
     * @param  string  $abstract
     * @param  \Closure|string|null  $concrete
     * @param  bool  $shared
     * @return void
     */
    public function bind($abstract, $concrete = null, $shared = false)
    {
        // 如果未提供實現類 $concrete,我們直接將抽象類作為實現 $abstract。
        // 這之後,我們無需明確指定 $abstract 和 $concrete 是否為單例模式,
        // 而是通過 $shared 標識來決定它們是單例還是每次都需要例項化處理。
        $this->dropStaleInstances($abstract);

        if (is_null($concrete)) {
            $concrete = $abstract;
        }

        // 如果繫結時傳入的實現類非閉包,即繫結時是直接給定了實現類的類名,
        // 這時要稍微處理下將類名封裝成一個閉包,保證解析時處理手法的統一。
        if (! $concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->bindings[$abstract] = compact('concrete', 'shared');

        // 最後如果抽象類已經被容器解析過,我們將觸發 rebound 監聽器。
        // 並且通過觸發 rebound 監聽器回撥,將任何已被解析過的服務更新最新的實現到抽象介面。
        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }

    /**
     * Get the Closure to be used when building a type. 當繫結實現為類名時,則封裝成閉包並返回。
     *
     * @param  string  $abstract
     * @param  string  $concrete
     * @return \Closure
     */
    protected function getClosure($abstract, $concrete)
    {
        return function ($container, $parameters = []) use ($abstract, $concrete) {
            if ($abstract == $concrete) {
                return $container->build($concrete);
            }

            return $container->make($concrete, $parameters);
        };
    }

    /**
     * Fire the "rebound" callbacks for the given abstract type. 依據給定的抽象服務介面,觸發其 "rebound" 回撥
     *
     * @param  string  $abstract
     * @return void
     */
    protected function rebound($abstract)
    {
        $instance = $this->make($abstract);

        foreach ($this->getReboundCallbacks($abstract) as $callback) {
            call_user_func($callback, $this, $instance);
        }
    }

    /**
     * Get the rebound callbacks for a given type. 獲取給定抽象服務的回撥函式。
     *
     * @param  string  $abstract
     * @return array
     */
    protected function getReboundCallbacks($abstract)
    {
        if (isset($this->reboundCallbacks[$abstract])) {
            return $this->reboundCallbacks[$abstract];
        }

        return [];
    }

bind 方法中,主要完成以下幾個方面的處理:

  • 幹掉之前解析過的服務例項;
  • 將繫結的實現類封裝成閉包,以確保後續處理的統一;
  • 針對已解析過的服務例項,再次觸發重新繫結回撥函式,同時將最新的實現類更新到介面裡面。

在繫結過程中,服務容器並不會執行服務的解析操作,這樣有利於提升服務的效能。直到在專案執行期間,被使用時才會真正解析出需要使用的對應服務,實現「按需載入」。

make 解析處理

解析處理和繫結一樣定義在 Illuminate\Container\Container 類中,無論是手動解析還是通過自動注入的方式,實現原理都是基於 PHP 的反射機制。

所有我們還是直接從 make 方法開始去挖出相關細節:

    /**
     * Resolve the given type from the container. 從容器中解析出給定服務具體實現
     *
     * @param  string  $abstract
     * @param  array  $parameters
     * @return mixed
     */
    public function make($abstract, array $parameters = [])
    {
        return $this->resolve($abstract, $parameters);
    }

    /**
     * Resolve the given type from the container. 從容器中解析出給定服務具體實現
     *
     * @param  string  $abstract
     * @param  array  $parameters
     * @return mixed
     */
    protected function resolve($abstract, $parameters = [])
    {
        $abstract = $this->getAlias($abstract);

        // 如果繫結時基於上下文繫結,此時需要解析出上下文實現類
        $needsContextualBuild = ! empty($parameters) || ! is_null(
            $this->getContextualConcrete($abstract)
        );

        // 如果給定的型別已單例模式繫結,直接從服務容器中返回這個例項而無需重新例項化
        if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];
        }

        $this->with[] = $parameters;

        $concrete = $this->getConcrete($abstract);

        // 已準備就緒建立這個繫結的例項。下面將例項化給定例項及內嵌的所有依賴例項。
        // 到這裡我們已經做好建立例項的準備工作。只有可以構建的服務才可以執行 build 方法去例項化服務;
        // 否則也就是說我們的服務還存在依賴,然後不斷的去解析巢狀的依賴,知道它們可以去構建(isBuildable)。
        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

        // 如果我們的服務存在擴充套件(extend)繫結,此時就需要去執行擴充套件。
        // 擴充套件繫結適用於修改服務的配置或者修飾(decorating)服務實現。
        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }

        // 如果我們的服務已單例模式繫結,此時無要將已解析的服務快取到單例物件池中(instances),
        // 後續便可以直接獲取單例服務物件了。
        if ($this->isShared($abstract) && ! $needsContextualBuild) {
            $this->instances[$abstract] = $object;
        }

        $this->fireResolvingCallbacks($abstract, $object);

        $this->resolved[$abstract] = true;

        array_pop($this->with);

        return $object;
    }

    /**
     * Determine if the given concrete is buildable. 判斷給定的實現是否立馬進行構建
     *
     * @param  mixed   $concrete
     * @param  string  $abstract
     * @return bool
     */
    protected function isBuildable($concrete, $abstract)
    {
        // 僅當實現類和介面相同或者實現為閉包時可構建
        return $concrete === $abstract || $concrete instanceof Closure;
    }

    /**
     * Instantiate a concrete instance of the given type. 構建(例項化)給定型別的實現類(匿名函式)例項
     *
     * @param  string  $concrete
     * @return mixed
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    public function build($concrete)
    {
        // 如果給定的實現是一個閉包,直接執行並閉包,返回執行結果
        if ($concrete instanceof Closure) {
            return $concrete($this, $this->getLastParameterOverride());
        }

        $reflector = new ReflectionClass($concrete);

        // 如果需要解析的類無法例項化,即試圖解析一個抽象類型別如: 介面或抽象類而非實現類,直接丟擲異常。
        if (! $reflector->isInstantiable()) {
            return $this->notInstantiable($concrete);
        }

        $this->buildStack[] = $concrete;

        // 通過反射獲取實現類建構函式
        $constructor = $reflector->getConstructor();

        // 如果實現類並沒有定義建構函式,說明這個實現類沒有相關依賴。
        // 我們可以直接例項化這個實現類,而無需自動解析依賴(自動注入)。
        if (is_null($constructor)) {
            array_pop($this->buildStack);

            return new $concrete;
        }

        // 獲取到實現類建構函式依賴引數
        $dependencies = $constructor->getParameters();

        // 解析出所有依賴
        $instances = $this->resolveDependencies(
            $dependencies
        );

        array_pop($this->buildStack);

        // 這是我們就可以建立服務例項並返回。
        return $reflector->newInstanceArgs($instances);
    }

    /**
     * Resolve all of the dependencies from the ReflectionParameters. 從 ReflectionParameters 解析出所有建構函式所需依賴
     *
     * @param  array  $dependencies
     * @return array
     */
    protected function resolveDependencies(array $dependencies)
    {
        $results = [];

        foreach ($dependencies as $dependency) {
            // If this dependency has a override for this particular build we will use
            // that instead as the value. Otherwise, we will continue with this run
            // of resolutions and let reflection attempt to determine the result.
            if ($this->hasParameterOverride($dependency)) {
                $results[] = $this->getParameterOverride($dependency);

                continue;
            }

            // 建構函式引數為非類時,即引數為 string、int 等標量型別或閉包時,按照標量和閉包解析;
            // 否則需要解析類。
            $results[] = is_null($dependency->getClass())
                            ? $this->resolvePrimitive($dependency)
                            : $this->resolveClass($dependency);
        }

        return $results;
    }

    /**
     * Resolve a non-class hinted primitive dependency. 依據型別提示解析出標量型別(閉包)資料
     *
     * @param  \ReflectionParameter  $parameter
     * @return mixed
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    protected function resolvePrimitive(ReflectionParameter $parameter)
    {
        if (! is_null($concrete = $this->getContextualConcrete('$'.$parameter->name))) {
            return $concrete instanceof Closure ? $concrete($this) : $concrete;
        }

        if ($parameter->isDefaultValueAvailable()) {
            return $parameter->getDefaultValue();
        }

        $this->unresolvablePrimitive($parameter);
    }

    /**
     * Resolve a class based dependency from the container. 從服務容器中解析出類依賴(自動注入)
     *
     * @param  \ReflectionParameter  $parameter
     * @return mixed
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    protected function resolveClass(ReflectionParameter $parameter)
    {
        try {
            return $this->make($parameter->getClass()->name);
        }

        catch (BindingResolutionException $e) {
            if ($parameter->isOptional()) {
                return $parameter->getDefaultValue();
            }

            throw $e;
        }
    }

以上,便是 Laravel 服務容器解析的核心,得益於 PHP 的反射機制,實現了自動依賴注入和服務解析處理,概括起來包含以下步驟:

    1. 對於單例繫結資料如果一解析過服務則直接返回,否則繼續執行解析;
    1. 非單例繫結的服務型別,通過介面獲取繫結實現類;
    1. 介面即服務或者閉包時進行構建(build)處理,構建時依託於 PHP 反射機制進行自動依賴注入解析出完整的服務例項物件;否則繼續解析(make)出所有巢狀的依賴;
    1. 如果服務存在擴充套件繫結,解析出擴充套件繫結結果;
    1. 如果繫結服務為單例繫結型別(singleton),將解析到的服務加入到單例物件池;
    1. 其它處理如觸發繫結監聽器、將服務標記為已解析狀態等,並返回服務例項。

更多細節處理還是需要我們進一步深入的核心中才能發掘出來,但到這其實已經差不太多了。有興趣的朋友可以親自了解下其它繫結方法的原始碼解析處理。

以上便是今天 Laravel 服務容器的全部內容,希望對大家有所啟發。

資料

感謝一下優秀的學習資料:

https://www.insp.top/learn-laravel-contain...

部落格:深度挖掘 Laravel 生命週期

部落格:Laravel 核心——IoC 服務容器

https://hk.saowen.com/a/6c880512a3a01a10b0...

http://rrylee.github.io/2015/09/23/laravel...

https://blog.tanteng.me/2016/01/laravel-co...

https://juejin.im/entry/5916a557a0bb9f005f...

教程:從 1 行程式碼開始,帶你係統性地理解 Service Container 核心概念

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章