深刻講解 Laravel 的 IoC 服務容器

 

衆所周知,Laravel 控制反轉 (IoC) / 依賴注入 (DI) 的功能很是強大。遺憾的是, 官方文檔 並無詳細講解它的全部功能,因此我決定本身實踐一下,並整理成文。下面的代碼是基於 Laravel 5.4.26 的,其餘版本可能會有所不一樣。php

瞭解依賴注入

我在這裏不會詳細講解依賴注入/控制反轉的原則 - 若是你對此還不是很瞭解,建議閱讀 Fabien Potencier (Symfony 框架的創始人)的 What is Dependency Injection?html

 

訪問容器

經過 Laravel 訪問 Container 實例的方式有不少種,最簡單的就是調用輔助函數 app()laravel

1 $container = app();

 

爲了突出重點 Container 類,這裏就不贅述其餘方式了。git

注意: 官方文檔中使用的是 $this->app 而不是 $containergithub

(* 在 Laravel 應用中,Application 其實是 Container 的一個子類 ( 這也說明了輔助函數 app() 的由來 ),不過在這篇文章中我仍是將重點講解 Container 類的方法。)sql

在 Laravel 以外使用 Illuminate\Container

想要不基於 Laravel 使用 Container,安裝 而後:shell

use Illuminate\Container\Container;

$container = Container::getInstance();

 

基礎用法

最簡單的用法是經過構造函數注入依賴類。數組

1 class MyClass
2 {
3     private $dependency;
4 
5     public function __construct(AnotherClass $dependency)
6     {
7         $this->dependency = $dependency;
8     }
9 }

 

使用 Container 的 make() 方法實例化 MyClass 類:緩存

$instance = $container->make(MyClass::class);

container 會自動實例化依賴類,因此上面代碼實現的功能就至關於:服務器

$instance = new MyClass(new AnotherClass());

( 假設 AnotherClass 還有須要依賴的類 - 在這種狀況下,Container 會遞歸式地實例化全部的依賴。)

實戰

 phper在進階的時候總會遇到一些問題和瓶頸,業務代碼寫多了沒有方向感,不知道該從那裏入手去提高,對此我整理了一些資料,包括但不限於:分佈式架構、高可擴展、高性能、高併發、服務器性能調優、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql優化、shell腳本、Docker、微服務、Nginx等多個知識點高級進階乾貨須要的能夠免費分享給你們須要的(點擊→)個人官方羣677079770

下面是一些基於 PHP-DI 文檔 的例子 - 將發送郵件與用戶註冊的代碼解耦:

 1 class Mailer
 2 {
 3     public function mail($recipient, $content)
 4     {
 5         // 發送郵件
 6         // ...
 7     }
 8 }
 9 class UserManager
10 {
11     private $mailer;
12 
13     public function __construct(Mailer $mailer)
14     {
15         $this->mailer = $mailer;
16     }
17 
18     public function register($email, $password)
19     {
20         // 建立用戶帳號
21         // ...
22 
23         // 給用戶發送問候郵件
24         $this->mailer->mail($email, 'Hello and welcome!');
25     }
26 }
27 use Illuminate\Container\Container;
28 
29 $container = Container::getInstance();
30 
31 $userManager = $container->make(UserManager::class);
32 $userManager->register('dave@davejamesmiller.com', 'MySuperSecurePassword!');

 

綁定接口與具體實現

經過 Container 類,咱們能夠輕鬆實現從接口到具體類到實例的過程。首先定義接口:

interface MyInterface { /* ... */ }
interface AnotherInterface { /* ... */ }

 

聲明實現接口的具體類,具體類還能夠依賴其餘接口( 或者是像上個例子中的具體類 ):

class MyClass implements MyInterface
1 {
2     private $dependency;
3 
4     public function __construct(AnotherInterface $dependency)
5     {
6         $this->dependency = $dependency;
7     }
8 }

 

而後使用 bind() 方法把接口與具體類進行綁定:

$container->bind(MyInterface::class, MyClass::class);
$container->bind(AnotherInterface::class, AnotherClass::class);

 

最後,在 make() 方法中,使用接口做爲參數:

$instance = $container->make(MyInterface::class);

 

注意: 若是沒有將接口與具體類進行綁定操做,就會報錯:

Fatal error: Uncaught ReflectionException: Class MyInterface does not exist

 

這是由於 container 會嘗試實例化接口 ( new MyInterface),這自己在語法上就是錯誤的。

實戰

可更換的緩存層:

 1 interface Cache
 2 {
 3     public function get($key);
 4     public function put($key, $value);
 5 }
 6 class RedisCache implements Cache
 7 {
 8     public function get($key) { /* ... */ }
 9     public function put($key, $value) { /* ... */ }
10 }
11 class Worker
12 {
13     private $cache;
14 
15     public function __construct(Cache $cache)
16     {
17         $this->cache = $cache;
18     }
19 
20     public function result()
21     {
22         // 應用緩存
23         $result = $this->cache->get('worker');
24 
25         if ($result === null) {
26             $result = do_something_slow();
27 
28             $this->cache->put('worker', $result);
29         }
30 
31         return $result;
32     }
33 }
34 use Illuminate\Container\Container;
35 
36 $container = Container::getInstance();
37 $container->bind(Cache::class, RedisCache::class);
38 
39 $result = $container->make(Worker::class)->result();

 

綁定抽象類與具體類

也能夠與抽象類進行綁定:

$container->bind(MyAbstract::class, MyConcreteClass::class);

 

或者將具體類與其子類進行綁定:

$container->bind(MySQLDatabase::class, CustomMySQLDatabase::class);

 

自定義綁定

在使用 bind() 方法進行綁定操做時,若是某個類須要額外的配置,還經過閉包函數來實現:

$container->bind(Database::class, function (Container $container) {
    return new MySQLDatabase(MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS);
});

 

每次帶着配置信息建立一個 MySQLDatabase 類的實例的時候( 下面後講到如何經過 Singletons 建立一個能夠共享的實例),都要用到 Database 接口。咱們看到閉包函數接收了 Container 的實例做爲參數,若是須要的話,還能夠用它來實例化其餘類:

1 $container->bind(Logger::class, function (Container $container) {
2     $filesystem = $container->make(Filesystem::class);
3 
4     return new FileLogger($filesystem, 'logs/error.log');
5 });

 

還能夠經過閉包函數自定義要如何實例化某個類:

1 $container->bind(GitHub\Client::class, function (Container $container) {
2     $client = new GitHub\Client;
3     $client->setEnterpriseUrl(GITHUB_HOST);
4     return $client;
5 });

 

解析回調函數

可使用 resolving() 方法來註冊一個回調函數,當綁定被解析的時候,就調用這個回調函數:

1 $container->resolving(GitHub\Client::class, function ($client, Container $container) {
2     $client->setEnterpriseUrl(GITHUB_HOST);
3 });

 

全部的註冊的回調函數都會被調用。這種方法也適用於接口和抽象類:

 1 $container->resolving(Logger::class, function (Logger $logger) {
 2     $logger->setLevel('debug');
 3 });
 4 
 5 $container->resolving(FileLogger::class, function (FileLogger $logger) {
 6     $logger->setFilename('logs/debug.log');
 7 });
 8 
 9 $container->bind(Logger::class, FileLogger::class);
10 
11 $logger = $container->make(Logger::class);

 

還能夠註冊一個任何類被解析時都會被調用的回調函數 - 可是我想這可能僅適用於登陸和調試:

1 $container->resolving(function ($object, Container $container) {
2     // ...
3 });

 

擴展類

你還可使用 extend() 方法把一個類與另外一個類的實例進行綁定:

1 $container->extend(APIClient::class, function ($client, Container $container) {
2     return new APIClientDecorator($client);
3 });

 

這裏返回的另一個類應該也實現了一樣的接口,不然會報錯。

單例綁定

只要使用 bind() 方法進行綁定,每次用的時候,就會建立一個新的實例( 閉包函數就會被調用一次)。爲了共用一個實例,可使用 singleton() 方法來代替 bind() 方法:

$container->singleton(Cache::class, RedisCache::class);

 

或者是閉包:

1 $container->singleton(Database::class, function (Container $container) {
2     return new MySQLDatabase('localhost', 'testdb', 'user', 'pass');
3 });

 

爲一個具體類建立單例,就只傳這個類做爲惟一的參數:

$container->singleton(MySQLDatabase::class);

 

在以上的每種狀況下,單例對象都是一次建立,反覆使用。若是想要複用的實例已經生成了,則可使用 instance() 方法。例如,Laravel 就是用這種方式來確保Container 的實例有且僅有一個的:

$container->instance(Container::class, $container);

 

自定義綁定的名稱

其實,你可使用任意字符串做爲綁定的名稱,而不必定非要用類名或者接口名 - 可是這樣作的弊端就是不能使用類名實例化了,而只能使用 make() 方法:

$container->bind('database', MySQLDatabase::class);

$db = $container->make('database');

 

爲了同時支持類和接口,而且簡化類名的寫法,可使用 alias() 方法:

1 $container->singleton(Cache::class, RedisCache::class);
2 $container->alias(Cache::class, 'cache');
3 
4 $cache1 = $container->make(Cache::class);
5 $cache2 = $container->make('cache');
6 
7 assert($cache1 === $cache2);

 

存儲值

你也可使用 container 來存儲任何值 - 好比:配置數據:

$container->instance('database.name', 'testdb');

$db_name = $container->make('database.name');

 

支持以數組的形式存儲:

$container['database.name'] = 'testdb';

$db_name = $container['database.name'];

 

在經過閉包進行綁定的時候,這種存儲方式就顯示出其好用之處了:

$container->singleton('database', function (Container $container) {
    return new MySQLDatabase(
        $container['database.host'],
        $container['database.name'],
        $container['database.user'],
        $container['database.pass']
    );
});

 

( Laravel 框架沒有用 container 來存儲配置文件,而是用了單獨的 Config 類 - 可是 PHP-DI 用了)

小貼士: 在實例化對象的時候,還能夠用數組的形式來代替 make() 方法:

$db = $container['database'];

 

經過方法 / 函數作依賴注入

到目前爲止,咱們已經看了不少經過構造函數進行依賴注入的例子,其實,Laravel 還支持對任何方法作依賴注入:

function do_something(Cache $cache) { /* ... */ }

$result = $container->call('do_something');

 

除了依賴類,還能夠傳其餘參數:

1 function show_product(Cache $cache, $id, $tab = 'details') { /* ... */ }
2 
3 // show_product($cache, 1)
4 $container->call('show_product', [1]);
5 $container->call('show_product', ['id' => 1]);
6 
7 // show_product($cache, 1, 'spec')
8 $container->call('show_product', [1, 'spec']);
9 $container->call('show_product', ['id' => 1, 'tab' => 'spec']);

 

可用於任何可調用的方法:

閉包

1 $closure = function (Cache $cache) { /* ... */ };
2 
3 $container->call($closure);

 

靜態方法

1 class SomeClass
2 {
3     public static function staticMethod(Cache $cache) { /* ... */ }
4 }
5 $container->call(['SomeClass', 'staticMethod']);
6 // 或者:
7 $container->call('SomeClass::staticMethod');

 

普通方法

1 class PostController
2 {
3     public function index(Cache $cache) { /* ... */ }
4     public function show(Cache $cache, $id) { /* ... */ }
5 }
6 $controller = $container->make(PostController::class);
7 
8 $container->call([$controller, 'index']);
9 $container->call([$controller, 'show'], ['id' => 1]);

 

調用實例方法的快捷方式

經過這種語法結構 ClassName@methodName,就 能夠達到實例化一個類並調用其方法的目:

1 $container->call('PostController@index');
2 $container->call('PostController@show', ['id' => 4]);

 

容器用於實例化類,這意味着:

  1. 依賴項被注入構造函數(以及方法)。
  2. 若是但願重用這個類,則能夠將該類定義爲單例類。
  3. 你可使用接口或任意名稱,而不是具體的類。

例如,這將會啓做用:

1 class PostController
2 {
3     public function __construct(Request $request) { /* ... */ }
4     public function index(Cache $cache) { /* ... */ }
5 }
6 $container->singleton('post', PostController::class);
7 $container->call('post@index');

 

最後,你能夠將「默認方法」做爲第三個參數。若是第一個參數是一個沒有指定方法的類名,則將調用默認的方法。 Laravel 使用 事件處理 來實現:

1 $container->call(MyEventHandler::class, $parameters, 'handle');
2 
3 // Equivalent to:
4 $container->call('MyEventHandler@handle', $parameters);

 

方法調用綁定

可使用 bindMethod() 方法重寫方法調用,例如傳遞其餘參數:

1 $container->bindMethod('PostController@index', function ($controller, $container) {
2     $posts = get_posts(...);
3 
4     return $controller->index($posts);
5 });

 

全部這些都會奏效,調用閉包而不是的原始方法:

1 $container->call('PostController@index');
2 $container->call('PostController', [], 'index');
3 $container->call([new PostController, 'index']);

 

可是, call() 的任何附加參數都不會傳遞到閉包中,所以不能使用它們。

1 $container->call('PostController@index', ['Not used :-(']);

 

注意: 這個方法不屬於 容器接口, 只是具體的 容器類. 參考 提交的 PR 瞭解爲何忽略參數。

上下文綁定

有時候,你但願在不一樣的地方使用接口的不一樣實現。下面是來自 Laravel 文檔 中的一個例子:

1 $container
2     ->when(PhotoController::class)
3     ->needs(Filesystem::class)
4     ->give(LocalFilesystem::class);
5 
6 $container
7     ->when(VideoController::class)
8     ->needs(Filesystem::class)
9     ->give(S3Filesystem::class);

 

如今, PhotoController 和 VideoController 均可以依賴於文件系統接口,可是每一個都將接收不一樣的實現。你還能夠爲 give() 使用閉包,就像使用 bind() 同樣:

1 $container
2     ->when(VideoController::class)
3     ->needs(Filesystem::class)
4     ->give(function () {
5         return Storage::disk('s3');
6     });

 

或者命名依賴項:

1 $container->instance('s3', $s3Filesystem);
2 
3 $container
4     ->when(VideoController::class)
5     ->needs(Filesystem::class)
6     ->give('s3');

 

將參數綁定基本類型

你還能夠經過將變量名稱傳遞給 needs()(而不是接口)並將值傳遞給 give() 來綁定基本類型(字符串,整數等):

1 $container
2     ->when(MySQLDatabase::class)
3     ->needs('$username')
4     ->give(DB_USER);

 

您可使用閉包來延遲檢索值,直到須要它:

1 $container
2     ->when(MySQLDatabase::class)
3     ->needs('$username')
4     ->give(function () {
5         return config('database.user');
6     });

 

在這裏你不能傳遞一個類或一個命名的依賴項(例如 give('database.user'))由於它將做爲文字值返回 - 爲此你必須使用一個閉包:

1 $container
2     ->when(MySQLDatabase::class)
3     ->needs('$username')
4     ->give(function (Container $container) {
5         return $container['database.user'];
6     });

 

標記

你可使用容器 tag 來綁定相關標記:

$container->tag(MyPlugin::class, 'plugin');
$container->tag(AnotherPlugin::class, 'plugin');

 

而後將全部標記的實例檢索爲數組:

1 foreach ($container->tagged('plugin') as $plugin) {
2     $plugin->init();
3 }

 

tag() 的參數都接受數組:

1 $container->tag([MyPlugin::class, AnotherPlugin::class], 'plugin');
2 $container->tag(MyPlugin::class, ['plugin', 'plugin.admin']);

 

從新綁定

*Note: 這是一個更高級的,只是不多須要-請隨意跳過它! *

在綁定或實例已經被使用後須要更改時,能夠調用 rebinding() 回調 - 例如,此 Session 類在被 Auth 類使用後被替換,所以須要通知 Auth 類變化:

 1 $container->singleton(Auth::class, function (Container $container) {
 2     $auth = new Auth;
 3     $auth->setSession($container->make(Session::class));
 4 
 5     $container->rebinding(Session::class, function ($container, $session) use ($auth) {
 6         $auth->setSession($session);
 7     });
 8 
 9     return $auth;
10 });
11 
12 $container->instance(Session::class, new Session(['username' => 'dave']));
13 $auth = $container->make(Auth::class);
14 echo $auth->username(); // dave
15 $container->instance(Session::class, new Session(['username' => 'danny']));
16 
17 echo $auth->username(); // danny

 

(有關從新綁定的更多信息, 看 這裏這裏.)

refresh()

還有一個快捷方法 refresh() 來處理這個常見模式:

1 $container->singleton(Auth::class, function (Container $container) {
2     $auth = new Auth;
3     $auth->setSession($container->make(Session::class));
4 
5     $container->refresh(Session::class, $auth, 'setSession');
6 
7     return $auth;
8 });

 

它還返回現有實例或綁定(若是有的話),所以您能夠這樣作:

1 // This only works if you call singleton() or bind() on the class
2 $container->singleton(Session::class);
3 
4 $container->singleton(Auth::class, function (Container $container) {
5     $auth = new Auth;
6     $auth->setSession($container->refresh(Session::class, $auth, 'setSession'));
7     return $auth;
8 });

 

(就我的而言,我發現這種語法更加混亂,而且更喜歡上面更詳細的版本!)

Note: 這些方法不屬於 Container interface, 只有具體 Container class.

覆蓋構造函數參數

makeWith() 方法容許你將其餘參數傳遞給構造函數。 它忽略任何現有的實例或單例,而且在建立具備不一樣參數的類的多個實例時仍然有用,同時仍然注入依賴項:

1 class Post
2 {
3     public function __construct(Database $db, int $id) { /* ... */ }
4 }
5 $post1 = $container->makeWith(Post::class, ['id' => 1]);
6 $post2 = $container->makeWith(Post::class, ['id' => 2]);

 

Note: 在Laravel 5.3及如下版本中,它很簡單 make($class, $parameters). 它是在 Laravel 5.4 被移除, 但後來 從新添加爲 makeWith() 在 5.4.16. 在Laravel 5.5中,它彷佛將恢復爲Laravel 5.3語法.

其餘方法

這涵蓋了我認爲有用的全部方法 - 但只是爲了解決問題,這裏是剩下的公共方法的摘要......

bound()

若是類或名稱已與 bind(), singleton(), instance() or alias() 綁定,則 bound() 返回true。

1 if (! $container->bound('database.user')) {
2     // ...
3 }
4 
5 還可使用數組訪問語法和 isset():
6 
7 if (! isset($container['database.user'])) {
8     // ...
9 }

 

它能夠用 unset() 重置,它刪除指定的綁定/實例/別名。

unset($container['database.user']);
var_dump($container->bound('database.user')); // false

 

bindIf()

bindIf()bind() 作一樣的事情,除了它只註冊一個綁定(若是尚未)(參考上面的 bound())。 它可能用於在包中註冊默認綁定,同時容許用戶覆蓋它。

$container->bindIf(Loader::class, FallbackLoader::class);

 

沒有 singletonIf() 方法,但你可使用 bindIf($abstract, $concrete, true) 代替:

$container->bindIf(Loader::class, FallbackLoader::class, true);

 

或者這樣寫全也能夠:

if (! $container->bound(Loader::class)) {
    $container->singleton(Loader::class, FallbackLoader::class);
}

 

resolved()

若是已經解析了類 resolved() 則返回true。

var_dump($container->resolved(Database::class)); // false
$container->make(Database::class);
var_dump($container->resolved(Database::class)); // true

 

我不肯定它有什麼用處,若是使用 unset() 它會被重置 (能夠看上面的 bound())。

unset($container[Database::class]);
var_dump($container->resolved(Database::class)); // false

 

factory()

factory() 方法返回一個不帶參數的閉包,並調用 make()

$dbFactory = $container->factory(Database::class);

$db = $dbFactory();

 

我不肯定它有什麼用處...

wrap()

wrap() 方法包裝一個閉包,以便在執行時注入它的依賴項。 wrap 方法接受一組參數, 返回的閉包沒有參數:

1 $cacheGetter = function (Cache $cache, $key) {
2     return $cache->get($key);
3 };
4 
5 $usernameGetter = $container->wrap($cacheGetter, ['username']);
6 
7 $username = $usernameGetter();

 

我不肯定它有什麼用處,由於閉包沒有參數...

Note: 這種方法不屬於 Container interface, 只屬於 Container class.

afterResolving()

afterResolving() 方法與 resolving() 徹底相同,只是在「解析」回調以後調用 「解析後」 回調。 我不肯定何時會有用...

最後...

  • isShared() - 肯定給定類型是否爲共享單例/實例
  • isAlias() - 肯定給定字符串是不是已註冊的別名
  • hasMethodBinding() - 肯定容器是否具備給定的方法綁定
  • getBindings() - 檢索全部已註冊綁定的原始數組
  • getAlias($abstract) - 解析基礎類/綁定名稱的別名
  • forgetInstance($abstract) - 清除單個實例對象
  • forgetInstances() - 清除全部實例對象
  • flush() - 清除全部綁定和實例,有效地重置容器
  • setInstance() - 替換 getInstance() 使用的實例(Tip:使用 setInstance(null) 清除它,因此下次它將生成一個新實例)

Note: 最後一節中沒有一個方法是其中的一部分 Container interface.

相關文章
相關標籤/搜索