1. 程式人生 > >Laravel 核心——IoC 服務容器

Laravel 核心——IoC 服務容器

服務容器

本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/laravel-source-analysis
在說 Ioc 容器之前,我們需要了解什麼是 Ioc 容器。

Laravel 服務容器是一個用於管理類依賴和執行依賴注入的強大工具。

在理解這句話之前,我們需要先了解一下服務容器的來龍去脈: laravel神奇的服務容器。這篇部落格告訴我們,服務容器就是工廠模式的升級版,對於傳統的工廠模式來說,雖然解耦了物件和外部資源之間的關係,但是工廠和外部資源之間卻存在了耦和。而服務容器在為物件建立了外部資源的同時,又與外部資源沒有任何關係,這個就是 Ioc 容器。

所謂的依賴注入和控制反轉: 依賴注入和控制反轉,就是

只要不是由內部生產(比如初始化、建構函式 __construct 中通過工廠方法、自行手動 new 的),而是由外部以引數或其他形式注入的,都屬於依賴注入(DI)

也就是說:

依賴注入是從應用程式的角度在描述,可以把依賴注入描述完整點:應用程式依賴容器建立並注入它所需要的外部資源;

控制反轉是從容器的角度在描述,描述完整點:容器控制應用程式,由容器反向的嚮應用程式注入應用程式所需要的外部資源。

Laravel中的服務容器
Laravel服務容器主要承擔兩個作用:繫結與解析,服務容器的結構如下:

繫結

所謂的繫結就是將介面與實現建立對應關係。幾乎所有的服務容器繫結都是在服務提供者中完成,也就是在服務提供者中繫結。

如果一個類沒有基於任何介面那麼就沒有必要將其繫結到容器。容器並不需要被告知如何構建物件,因為它會使用 PHP 的反射服務自動解析出具體的物件。

也就是說,如果需要依賴注入的外部資源如果沒有介面,那麼就不需要繫結,直接利用服務容器進行解析就可以了,服務容器會根據類名利用反射對其進行自動構造。

bind繫結

繫結有多種方法,首先最常用的是bind函式的繫結:

繫結自身

$this->app->bind('App\Services\RedisEventPusher', null);

繫結閉包

$this->app->bind('HelpSpot\API', function ($app) {
return new HelpSpot\API();
});//閉包直接提供實現方式

$this->app->bind('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});//需要依賴注入

繫結介面

$this->app->bind(
'App\Contracts\EventPusher',
'App\Services\RedisEventPusher'
);

這三種繫結方式中,第一種繫結自身一般用於繫結單例。

singleton繫結

singleton 方法繫結一個只需要解析一次的類或介面到容器,然後接下來對容器的呼叫將會返回同一個例項:

$this->app->singleton('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});

instance繫結

我們還可以使用 instance 方法繫結一個已存在的物件例項到容器,隨後呼叫容器將總是返回給定的例項:

  $api = new HelpSpot\API(new HttpClient);
  $this->app->instance('HelpSpot\Api', $api);

Context繫結

有時侯我們可能有兩個類使用同一個介面,但我們希望在每個類中注入不同實現,例如,兩個控制器依賴 Illuminate\Contracts\Filesystem\Filesystem 契約的不同實現。Laravel 為此定義了簡單、平滑的介面:

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

$this->app->when(StorageController::class)
          ->needs(Filesystem::class)
          ->give(function () {
            Storage::class
          });//提供類名

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
             return new Storage();
          });//提供實現方式

$this->app->when(VideoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
            return new Storage($app->make(Disk::class));
          });//需要依賴注入

原始值繫結

我們可能有一個接收注入類的類,同時需要注入一個原生的數值比如整型,可以結合上下文輕鬆注入這個類需要的任何值:

$this->app->when('App\Http\Controllers\UserController')
          ->needs('$variableName')
          ->give($value);

陣列繫結

app()['service'] = function(){
    return new Service();
};

標籤繫結

少數情況下,我們需要解析特定分類下的所有繫結,例如,你正在構建一個接收多個不同 Report 介面實現的報告聚合器,在註冊完 Report 實現之後,可以通過 tag 方法給它們分配一個標籤:

$this->app->bind('SpeedReport', function () {
  //
});

$this->app->bind('MemoryReport', function () {
  //
});

$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');

這些服務被打上標籤後,可以通過 tagged 方法來輕鬆解析它們:

$this->app->bind('ReportAggregator', function ($app) {
    return new ReportAggregator($app->tagged('reports'));
});

extend擴充套件

extend是在當原來的類被註冊或者例項化出來後,可以對其進行擴充套件:

public function testExtendInstancesArePreserved()
{
    $container = new Container;
    $container->bind('foo', function () {
        $obj = new StdClass;
        $obj->foo = 'bar';

        return $obj;
    });

    $obj = new StdClass;
    $obj->foo = 'foo';
    $container->instance('foo', $obj);

    $container->extend('foo', function ($obj, $container) {
        $obj->bar = 'baz';
        return $obj;
    });

    $container->extend('foo', function ($obj, $container) {
        $obj->baz = 'foo';
        return $obj;
    });

    $this->assertEquals('foo', $container->make('foo')->foo);
    $this->assertEquals('baz', $container->make('foo')->bar);
    $this->assertEquals('foo', $container->make('foo')->baz);
}

Rebounds與Rebinding

繫結是針對介面的,是為介面提供實現方式的方法。我們可以對介面在不同的時間段裡提供不同的實現方法,一般來說,對同一個介面提供新的實現方法後,不會對已經例項化的物件產生任何影響。但是在一些場景下,在提供新的介面實現後,我們希望對已經例項化的物件重新做一些改變,這個就是 rebinding 函式的用途。
下面就是一個例子:

abstract class Car
{
    public function __construct(Fuel $fuel)
    {
        $this->fuel = $fuel;
    }

    public function refuel($litres)
    {
        return $litres * $this->fuel->getPrice();
    }

    public function setFuel(Fuel $fuel)
    {
        $this->fuel = $fuel;
    }

}

class JeepWrangler extends Car
{
  //
}

interface Fuel
{
    public function getPrice();
}

class Petrol implements Fuel
{
    public function getPrice()
    {
        return 130.7;
    }
}

我們在服務容器中是這樣對car介面和fuel介面繫結的:

$this->app->bind('fuel', function ($app) {
    return new Petrol;
});

$this->app->bind('car', function ($app) {
    return new JeepWrangler($app['fuel']);
});

$this->app->make('car');

如果car被服務容器解析例項化成物件之後,有人修改了 fuel 介面的實現,從 Petrol 改為 PremiumPetrol:

$this->app->bind('fuel', function ($app) {
    return new PremiumPetrol;
});

由於 car 已經被例項化,那麼這個介面實現的改變並不會影響到 car 的實現,假若我們想要 car 的成員變數 fuel 隨著 fuel 介面的變化而變化,我們就需要一個回撥函式,每當對 fuel 介面實現進行改變的時候,都要對 car 的 fuel 變數進行更新,這就是 rebinding 的用途:

$this->app->bindShared('car', function ($app) {
    return new JeepWrangler($app->rebinding('fuel', function ($app, $fuel) {
        $app['car']->setFuel($fuel);
    }));
});

服務別名

什麼是服務別名
在說服務容器的解析之前,需要先說說服務的別名。什麼是服務別名呢?不同於上一個部落格中提到的 Facade 門面的別名(在 config/app 中定義),這裡的別名服務繫結名稱的別名。通過服務繫結的別名,在解析服務的時候,跟不使用別名的效果一致。別名的作用也是為了同時支援全型別的服務繫結名稱以及簡短的服務繫結名稱考慮的。

通俗的講,假如我們想要建立 auth 服務,我們既可以這樣寫:

$this->app->make('auth')

又可以寫成:

$this->app->make('\Illuminate\Auth\AuthManager::class')

還可以寫成

$this->app->make('\Illuminate\Contracts\Auth\Factory::class')

後面兩個服務的名字都是 auth 的別名,使用別名和使用 auth 的效果是相同的。

服務別名的遞迴

需要注意的是別名是可以遞迴的:

app()->alias('service', 'alias_a');
app()->alias('alias_a', 'alias_b');
app()-alias('alias_b', 'alias_c');

會得到:

'alias_a' => 'service'
'alias_b' => 'alias_a'
'alias_c' => 'alias_b'

服務別名的實現

那麼這些別名是如何載入到服務容器裡面的呢?實際上,服務容器裡面有個 aliases 陣列:

$aliases = [
  'app' => [\Illuminate\Foundation\Application::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::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],
...
]

而服務容器的初始化的過程中,會執行一個函式:

public function registerCoreContainerAliases()
{
  foreach ($aliases as $key => $aliases) {
    foreach ($aliases as $alias) {
      $this->alias($key, $alias);
    }
  }
}

public function alias($abstract, $alias)
{
  $this->aliases[$alias] = $abstract;

  $this->abstractAliases[$abstract][] = $alias;
}

載入後,服務容器的aliases和abstractAliases陣列:

$aliases = [
  'Illuminate\Foundation\Application' = "app"
  'Illuminate\Contracts\Container\Container' = "app"
  'Illuminate\Contracts\Foundation\Application' = "app"
  'Illuminate\Auth\AuthManager' = "auth"
  'Illuminate\Contracts\Auth\Factory' = "auth"
  'Illuminate\Contracts\Auth\Guard' = "auth.driver"
  'Illuminate\View\Compilers\BladeCompiler' = "blade.compiler"
  'Illuminate\Cache\CacheManager' = "cache"
  'Illuminate\Contracts\Cache\Factory' = "cache"
  ...
]
$abstractAliases = [
  app = {array} [3]
  0 = "Illuminate\Foundation\Application"
  1 = "Illuminate\Contracts\Container\Container"
  2 = "Illuminate\Contracts\Foundation\Application"
  auth = {array} [2]
  0 = "Illuminate\Auth\AuthManager"
  1 = "Illuminate\Contracts\Auth\Factory"
  auth.driver = {array} [1]
  0 = "Illuminate\Contracts\Auth\Guard"
  blade.compiler = {array} [1]
  0 = "Illuminate\View\Compilers\BladeCompiler"
  cache = {array} [2]
  0 = "Illuminate\Cache\CacheManager"
  1 = "Illuminate\Contracts\Cache\Factory"
  ...
]

服務解析

make 解析
有很多方式可以從容器中解析物件,首先,你可以使用 make 方法,該方法接收你想要解析的類名或介面名作為引數:

  $fooBar = $this->app->make('HelpSpot\API');

如果你所在的程式碼位置訪問不了 $app 變數,可以使用輔助函式resolve:

$api = resolve('HelpSpot\API');

自動注入

namespace App\Http\Controllers;

use App\Users\Repository as UserRepository;

class UserController extends Controller{
  /**
  * 使用者倉庫例項
  */
  protected $users;

  /**
  * 建立一個控制器例項
  *
  * @param UserRepository $users 自動注入
  * @return void
  */
  public function __construct(UserRepository $users)
  {
    $this->users = $users;
  }
}

call 方法注入

make 解析是服務容器進行解析構建類物件時所用的方法,在實際應用中,還有另外一個需求,那就是當前已經獲取了一個類物件,我們想要呼叫它的一個方法函式,這時發現這個方法中引數眾多,如果一個個的 make 會比較繁瑣,這個時候就要用到 call 解析了。我們可以看這個例子:

class TaskRepository{

    public function testContainerCall(User $user,Task $task){
        $task->where('user_id','!=',0)
            ->orderBy('created_at')
            ->get();

        $user->orderBy('created_at')
            ->get();
    }

    public static function testContainerCallStatic(User $user,Task $task){
        $task->where('user_id','!=',0)
            ->orderBy('created_at')
            ->get();

        $user->orderBy('created_at')
            ->get();
    }

    public function testCallback(){
        echo 'call callback successfully!';
    }

    public function testDefaultMethod(){
        echo 'default Method successfully!';
    }
}

靜態方法注入

服務容器的 call 解析主要依靠 call_user_func_array() 函式,關於這個函式可以檢視 Laravel學習筆記之Callback Type - 來生做個漫畫家,這個函式對類中的靜態函式和非靜態函式有一些區別,對於靜態函式來說:

class ContainerCallTest
{
    public function testContainerCallStatic(){
        App::call(TaskRepository::class.'@testContainerCallStatic');
        App::call(TaskRepository::class.'::testContainerCallStatic');
        App::call([TaskRepository::class,'testContainerCallStatic']);
    }
}

服務容器呼叫類的靜態方法有三種,注意第三種使用陣列的形式,陣列中可以直接傳類名 TaskRepository::class;

非靜態方法注入

對於類的非靜態方法:

class ContainerCallTest
{
    public function testContainerCall(){
        $taskRepo = new TaskRepository();
        App::call(TaskRepository::class.'@testContainerCall');
        App::call([$taskRepo,'testContainerCall']);
    }
}

我們可以看到非靜態方法只有兩種呼叫方式,而且第二種陣列傳遞的引數是類物件,原因就是 call_user_func_array函式的限制,對於非靜態方法只能傳遞物件。

bindmethod 方法繫結

服務容器還有一個 bindmethod 的方法,可以繫結類的一個方法到自定義的函式:

public function testContainCallMethodBind(){

    App::bindMethod(TaskRepository::class.'@testContainerCallStatic',function () {
         $taskRepo = new TaskRepository();
         $taskRepo->testCallback();
    });

    App::call(TaskRepository::class.'@testContainerCallStatic');
    App::call(TaskRepository::class.'::testContainerCallStatic');
    App::call([TaskRepository::class,'testContainerCallStatic']);

    App::bindMethod(TaskRepository::class.'@testContainerCall',function (TaskRepository $taskRepo) { $taskRepo->testCallback(); });

    $taskRepo = new TaskRepository();
    App::call(TaskRepository::class.'@testContainerCall');
    App::call([$taskRepo,'testContainerCall']);
}

從結果上看,bindmethod 不會對靜態的第二種解析方法( :: 解析方式)起作用,對於其他方式都會呼叫繫結的函式。

預設函式注入

public function testContainCallDefultMethod(){

    App::call(TaskRepository::class,[],'testContainerCall');

    App::call(TaskRepository::class,[],'testContainerCallStatic');

    App::bindMethod(TaskRepository::class.'@testContainerCallStatic',function () {
        $taskRepo = new TaskRepository();
        $taskRepo->testCallback();
    });

    App::bindMethod(TaskRepository::class.'@testContainerCall',function (TaskRepository $taskRepo) {  $taskRepo->testCallback(); });

    App::call(TaskRepository::class,[],'testContainerCall');

    App::call(TaskRepository::class,[],'testContainerCallStatic');

}

值得注意的是,這種預設函式注入的方法使得非靜態的方法也可以利用類名去呼叫,並不需要物件。預設函式注入也回受到 bindmethod 函式的影響。

陣列解析

  app()['service'];

app($service)的形式

  app('service');

服務容器事件

每當服務容器解析一個物件時就會觸發一個事件。你可以使用 resolving 方法監聽這個事件:

$this->app->resolving(function ($object, $app) {
  // 解析任何型別的物件時都會呼叫該方法...
});
$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
  // 解析「HelpSpot\API」型別的物件時呼叫...
});
$this->app->afterResolving(function ($object, $app) {
  // 解析任何型別的物件後都會呼叫該方法...
});
$this->app->afterResolving(HelpSpot\API::class, function ($api, $app) {
  // 解析「HelpSpot\API」型別的物件後呼叫...
});

服務容器每次解析物件的時候,都會呼叫這些通過 resolving 和 afterResolving 函式傳入的閉包函式,也就是觸發這些事件。
注意:如果是單例,則只在解析時會觸發一次

Written with StackEdit.

原文連結:
https://laravel-china.org/articles/4698/laravel-core-ioc-service-container