本文大部分翻譯自 DAVE JAMES MILLER 的 《Laravel’s Dependency Injection Container in Depth》 。php
上文介紹了 Dependency Injection Containers (容器)
的基本概念,如今接着深刻講解 Laravel
的 Container
。html
Laravel
中實現的 Inversion of Control (IoC) / Dependency Injection (DI) Container
很是強悍,但文檔中很低調的沒有細講它。laravel
本文中示例基於
Laravel 5.5
,其它版本差很少。git
Dependency Injection
關於 DI
請看這篇 《Laravel Dependency Injection (依賴注入) 概念詳解》,這裏再也不贅述。github
Container
Laravel
中有一大堆訪問 Container
實例的姿式,好比最簡單的:數組
$container = app();
但咱們仍是先關注下 Container
類自己。緩存
Laravel
官方文檔中通常使用$this->app
代替$container
。它是Application
類的實例,而Application
類繼承自Container
類。bash
Laravel
以外使用 Illuminate\Container
若是在 Laravel
以外session
mkdir container && cd container composer require illuminate/container
// 新建一個 container.php,文件名隨便取 <?php include './vendor/autoload.php'; use Illuminate\Container\Container; $container = Container::getInstance();
Container
的技能們type hint (類型提示)
注入
依賴:只須要在本身類的構造函數中使用 type hint
就實現 DI
:閉包
class MyClass { private $dependency; public function __construct(AnotherClass $dependency) { $this->dependency = $dependency; } }
接下來用 Container
的 make
方法來代替 new MyClass
:
$instance = $container->make(MyClass::class);
Container
會自動實例化依賴的對象,因此它等同於:
$instance = new MyClass(new AnotherClass());
若是 AnotherClass
也有 依賴
,那麼 Container
會遞歸注入它所需的依賴。
Container
使用 Reflection (反射) 來找到並實例化構造函數參數中的那些類,實現起來並不複雜,之後的文章裏再介紹。
下面是 PHP-DI 文檔 中的一個例子,它分離了「用戶註冊」和「發郵件」的過程:
class Mailer { public function mail($recipient, $content) { // Send an email to the recipient // ... } }
class UserManager { private $mailer; public function __construct(Mailer $mailer) { $this->mailer = $mailer; } public function register($email, $password) { // 建立用戶帳戶 // ... // 給用戶的郵箱發個 「hello" 郵件 $this->mailer->mail($email, 'Hello and welcome!'); } }
use Illuminate\Container\Container; $container = Container::getInstance(); $userManager = $container->make(UserManager::class); $userManager->register('dave@davejamesmiller.com', 'MySuperSecurePassword!');
用Container
能夠輕鬆地寫一個接口,而後在運行時實例化一個具體的實例。 首先定義接口:
interface MyInterface { /* ... */ } interface AnotherInterface { /* ... */ }
而後聲明實現這些接口的具體類。下面這個類不但實現了一個接口,還依賴了實現另外一個接口的類實例:
class MyClass implements MyInterface { private $dependency; // 依賴了一個實現 AnotherInterface 接口的類的實例 public function __construct(AnotherInterface $dependency) { $this->dependency = $dependency; } }
如今用 Container
的 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"。
下面是可封裝的 Cache
層:
interface Cache { public function get($key); public function put($key, $value); }
class Worker { private $cache; public function __construct(Cache $cache) { $this->cache = $cache; } public function result() { // 去緩存裏查詢 $result = $this->cache->get('worker'); if ($result === null) { // 若是緩存裏沒有,就去別的地方查詢,而後再放進緩存中 $result = do_something_slow(); $this->cache->put('worker', $result); } return $result; } }
use Illuminate\Container\Container; $container = Container::getInstance(); $container->bind(Cache::class, RedisCache::class); $result = $container->make(Worker::class)->result();
這裏用 Redis
作緩存,若是改用其餘緩存,只要把 RedisCache
換成別的就好了,easy!
綁定還能夠用在抽象類:
$container->bind(MyAbstract::class, MyConcreteClass::class);
或者繼承的類中:
$container->bind(MySQLDatabase::class, CustomMySQLDatabase::class);
若是類須要一些附加的配置項,能夠把 bind()
方法中的第二個參數換成 Closure (閉包函數)
:
$container->bind(Database::class, function (Container $container) { return new MySQLDatabase(MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS); });
閉包也可用於定製 具體類
的實例化方式:
$container->bind(GitHub\Client::class, function (Container $container) { $client = new GitHub\Client; $client->setEnterpriseUrl(GITHUB_HOST); return $client; });
可用 resolveing()
方法來註冊一個 callback (回調函數)
,而不是直接覆蓋掉以前的 綁定
。 這個函數會在綁定的類解析完成以後調用。
$container->resolving(GitHub\Client::class, function ($client, Container $container) { $client->setEnterpriseUrl(GITHUB_HOST); });
若是有一大堆 callbacks
,他們所有都會被調用。對於 接口
和 抽象類
也能夠這麼用:
$container->resolving(Logger::class, function (Logger $logger) { $logger->setLevel('debug'); }); $container->resolving(FileLogger::class, function (FileLogger $logger) { $logger->setFilename('logs/debug.log'); }); $container->bind(Logger::class, FileLogger::class); $logger = $container->make(Logger::class);
更 diao
的是,還能夠註冊成「什麼類解析完以後都調用」:
$container->resolving(function ($object, Container $container) { // ... });
但這個估計只有 logging
和 debugging
纔會用到。
使用 extend()
方法,能夠封裝一個類而後返回一個不一樣的對象 (裝飾模式):
$container->extend(APIClient::class, function ($client, Container $container) { return new APIClientDecorator($client); });
注意:這兩個類要實現相同的 接口
,否則用類型提示的時候會出錯:
interface Getable { public function get(); }
class APIClient implements Getable { public function get() { return 'yes!'; } }
class APIClientDecorator implements Getable { private $client; public function __construct(APIClient $client) { $this->client = $client; } public function get() { return 'no!'; } }
class User { private $client; public function __construct(Getable $client) { $this->client = $client; } }
$container->extend(APIClient::class, function ($client, Container $container) { return new APIClientDecorator($client); }); // $container->bind(Getable::class, APIClient::class); // 此時 $instance 的 $client 屬性已是 APIClentDecorator 類型了 $instance = $container->make(User::class);
使用 bind()
方法綁定後,每次解析時都會新實例化一個對象(或從新調用閉包),若是想獲取 單例
,則用 singleton()
方法代替 bind()
:
$container->singleton(Cache::class, RedisCache::class);
綁定單例 閉包
$container->singleton(Database::class, function (Container $container) { return new MySQLDatabase('localhost', 'testdb', 'user', 'pass'); });
綁定 具體類
的時候,不須要第二個參數:
$container->singleton(MySQLDatabase::class);
在每種狀況下,單例
對象將在第一次須要時建立,而後在後續重複使用。
若是你已經有一個 實例
而且想重複使用,能夠用 instance()
方法。 Laravel
就是用這種方法來確保每次獲取到的都是同一個 Container
實例:
$container->instance(Container::class, $container);
Container
還能夠綁定任意字符串而不是類/接口名稱。但這種狀況下不能使用類型提示,而且只能用 make()
來獲取實例。
$container->bind('database', MySQLDatabase::class); $db = $container->make('database');
爲了同時支持類/接口名稱和短名稱,可使用 alias()
:
$container->singleton(Cache::class, RedisCache::class); $container->alias(Cache::class, 'cache'); $cache1 = $container->make(Cache::class); $cache2 = $container->make('cache'); assert($cache1 === $cache2);
Container
還能夠用來保存任何值,例如 configuration
數據:
$container->instance('database.name', 'testdb'); $db_name = $container->make('database.name');
它支持數組訪問語法,這樣用起來更天然:
$container['database.name'] = 'testdb'; $db_name = $container['database.name'];
這是由於
Container
實現了 PHP 的 ArrayAccess 接口。
當處理 Closure
綁定的時候,你會發現這個方式很是好用:
$container->singleton('database', function (Container $container) { return new MySQLDatabase( $container['database.host'], $container['database.name'], $container['database.user'], $container['database.pass'] ); });
Laravel
本身沒有用這種方式來處理配置項,它使用了一個單獨的Config
類自己。PHP-DI
用了。
數組訪問語法還能夠代替 make()
來實例化對象:
$db = $container['database'];
除了給構造函數注入依賴,Laravel
還能夠往任意函數中注入:
function do_something(Cache $cache) { /* ... */ } $result = $container->call('do_something');
函數的附加參數能夠做爲索引或關聯數組傳遞:
function show_product(Cache $cache, $id, $tab = 'details') { /* ... */ } // show_product($cache, 1) $container->call('show_product', [1]); $container->call('show_product', ['id' => 1]); // show_product($cache, 1, 'spec') $container->call('show_product', [1, 'spec']); $container->call('show_product', ['id' => 1, 'tab' => 'spec']);
除此以外,閉包
:
$closure = function (Cache $cache) { /* ... */ }; $container->call($closure);
靜態方法
:
class SomeClass { public static function staticMethod(Cache $cache) { /* ... */ } }
$container->call(['SomeClass', 'staticMethod']); // or: $container->call('SomeClass::staticMethod');
實例的方法
:
class PostController { public function index(Cache $cache) { /* ... */ } public function show(Cache $cache, $id) { /* ... */ } }
$controller = $container->make(PostController::class); $container->call([$controller, 'index']); $container->call([$controller, 'show'], ['id' => 1]);
均可以注入。
使用 ClassName@methodName
語法能夠快捷調用實例中的方法:
$container->call('PostController@index'); $container->call('PostController@show', ['id' => 4]);
由於Container
被用來實例化類。意味着:
依賴
被注入進構造函數(或者方法);因此這樣調用也能夠生效:
class PostController { public function __construct(Request $request) { /* ... */ } public function index(Cache $cache) { /* ... */ } }
$container->singleton('post', PostController::class); $container->call('post@index');
最後,還能夠傳一個「默認方法」做爲第三個參數。若是第一個參數是沒有指定方法的類名稱,則將調用默認方法。 Laravel
用這種方式來處理 event handlers :
$container->call(MyEventHandler::class, $parameters, 'handle'); // 至關於: $container->call('MyEventHandler@handle', $parameters);
bindMethod()
方法可用來覆蓋方法,例如用來傳遞其餘參數:
$container->bindMethod('PostController@index', function ($controller, $container) { $posts = get_posts(...); return $controller->index($posts); });
下面的方式都有效,調用閉包來代替調用原始的方法:
$container->call('PostController@index'); $container->call('PostController', [], 'index'); $container->call([new PostController, 'index']);
可是,call()
的任何其餘參數都不會傳遞到閉包中,所以不能使用它們。
$container->call('PostController@index', ['Not used :-(']);
注意:這種方式不是 Container 接口 的一部分,只有在它的實現類 Container 纔有。在這個 PR` 裏能夠看到它加了什麼以及爲何參數被忽略。
有時候你想在不一樣的地方給接口不一樣的實現。這裏有 Laravel 文檔 裏的一個例子:
$container ->when(PhotoController::class) ->needs(Filesystem::class) ->give(LocalFilesystem::class); $container ->when(VideoController::class) ->needs(Filesystem::class) ->give(S3Filesystem::class);
如今 PhotoController
和 VideoController
都依賴了 Filesystem
接口,可是收到了不一樣的實例。
能夠像 bind()
那樣,給 give()
傳閉包:
->when(VideoController::class) ->needs(Filesystem::class) ->give(function () { return Storage::disk('s3'); });
或者短名稱:
$container->instance('s3', $s3Filesystem); $container ->when(VideoController::class) ->needs(Filesystem::class) ->give('s3');
當有一個類不只須要接受一個注入類,還須要注入一個基本值(好比整數)。
還能夠經過將變量名稱 (而不是接口) 傳遞給 needs()
並將值傳遞給 give()
來注入須要的任何值 (字符串、整數等) :
$container ->when(MySQLDatabase::class) ->needs('$username') ->give(DB_USER);
還可使用閉包實現延時加載,只在須要的時候取回這個 值
。
$container ->when(MySQLDatabase::class) ->needs('$username') ->give(function () { return config('database.user'); });
這種狀況下,不能傳遞類或命名的依賴關係(例如,give('database.user')),由於它將做爲字面值返回。因此須要使用閉包:
$container ->when(MySQLDatabase::class) ->needs('$username') ->give(function (Container $container) { return $container['database.user']; });
Container
能夠用來「標記」有關係的綁定:
$container->tag(MyPlugin::class, 'plugin'); $container->tag(AnotherPlugin::class, 'plugin');
這樣會以數組的形式取回全部「標記」的實例:
foreach ($container->tagged('plugin') as $plugin) { $plugin->init(); }
tag()
方法的兩個參數均可以接受數組:
$container->tag([MyPlugin::class, AnotherPlugin::class], 'plugin'); $container->tag(MyPlugin::class, ['plugin', 'plugin.admin']);
這個功能不多用到,能夠跳過,僅供參考。
在綁定或實例被使用以後又發生了變化,將調用一個 rebinding
方法。 下例中, Auth
使用 Session
類後,Session
類將被替換,此時須要通知 Auth
類這個變更:
$container->singleton(Auth::class, function (Container $container) { $auth = new Auth; $auth->setSession($container->make(Session::class)); $container->rebinding(Session::class, function ($container, $session) use ($auth) { $auth->setSession($session); }); return $auth; }); $container->instance(Session::class, new Session(['username' => 'dave'])); $auth = $container->make(Auth::class); echo $auth->username(); // dave $container->instance(Session::class, new Session(['username' => 'danny'])); echo $auth->username(); // danny
Rebinding
的更多信息能夠看這兩個連接:
https://stackoverflow.com/questions/38974593/laravels-ioc-container-rebinding-abstract-types
https://code.tutsplus.com/tutorials/digging-in-to-laravels-ioc-container--cms-22167
還有一個 refresh()
方法來處理這種模式:
$container->singleton(Auth::class, function (Container $container) { $auth = new Auth; $auth->setSession($container->make(Session::class)); $container->refresh(Session::class, $auth, 'setSession'); return $auth; });
它還返回現有的實例或綁定(若是有的話),因此能夠這樣作:
// This only works if you call singleton() or bind() on the class $container->singleton(Session::class); $container->singleton(Auth::class, function (Container $container) { $auth = new Auth; $auth->setSession($container->refresh(Session::class, $auth, 'setSession')); return $auth; });
注意:這種方式不是 Container 接口 的一部分,只有在它的實現類 Container 纔有。
makeWith
方法容許將附加參數傳遞給構造函數。它忽略任何現有的實例或單例,能夠用於建立具備不一樣參數的類的多個實例,同時仍然注入依賴關係:
class Post { public function __construct(Database $db, int $id) { /* ... */ } }
$post1 = $container->makeWith(Post::class, ['id' => 1]); $post2 = $container->makeWith(Post::class, ['id' => 2]);
注意:
Laravel 5.3
及如下使用make($class, $parameters)
。Laravel 5.4
中移除了此方法,可是在5.4.16
之後又從新加回來了makeWith()
。
這涵蓋了我認爲有用的全部方法,但僅僅是簡介,否則這篇文章就寫不完了。。。
若是一個類/名稱已經被 bind()
, singleton()
,instance()
或 alias()
綁定,那麼 bound()
方法返回 true
。
if (! $container->bound('database.user')) { // ... }
還可使用數組訪問語法和 isset()
:
if (! isset($container['database.user'])) { // ... }
可使用 unset()
來重置它,這會刪除指定的綁定/實例/別名。
unset($container['database.user']); var_dump($container->bound('database.user')); // false
bindIf()
和 bind()
功能相似,差異在於只有在現有綁定不存在的狀況下才註冊綁定。 它通常被用在 package
中註冊一個可被用戶重寫的默認綁定。
$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()
方法會返回 true
。
var_dump($container->resolved(Database::class)); // false $container->make(Database::class); var_dump($container->resolved(Database::class)); // true
我也不太肯定他用在什麼地方。。。
若是用過 unset()
以後它會被重置:
unset($container[Database::class]); var_dump($container->resolved(Database::class)); // false
factory()
方法返回一個不須要參數並調用 make()
的閉包。
$dbFactory = $container->factory(Database::class); $db = $dbFactory();
這個東西我也不知道有什麼用。。。
wrap
方法包裝一個閉包,以便在執行時依賴關係被注入。 它接受一個數組參數; 返回的閉包不帶參數:
$cacheGetter = function (Cache $cache, $key) { return $cache->get($key); }; $usernameGetter = $container->wrap($cacheGetter, ['username']); $username = $usernameGetter();
我也不知道它有啥用,由於返回的閉包沒帶回參數。。。
注意:這個方法不是 Container 接口` 的一部分,只有在它的實現類 Container 纔有。
afterResolving()
方法做用與 resolving()
徹底相同,不一樣之處是 調用 「resolving」回調以後再調用 「afterResolving」回調。
不知道何時會用到它。。。
isShared()
– 肯定一個給定的類型是一個 singleton/instanceisAlias()
– 肯定給定的字符串是不是已註冊的 別名
hasMethodBinding()
- 肯定容器是否具備給定的 method binding
getBindings()
- 取回全部已註冊綁定的原始數組getAlias($abstract)
- 獲取基礎類/綁定名稱的別名forgetInstance($abstract)
- 清除單個實例對象forgetInstances()
- 清除全部實例對象flush()
- 清除全部綁定和實例,有效地重置容器setInstance()
- 替換 getInstance()
使用的實例 (提示:使用 setInstance(null)來清除它,這樣下一次它將生成一個新的實例)
注意:這些方法不是 Container 接口 的一部分,只有在它的實現類 Container 纔有。
原創。 全部 Laravel 文章均已收錄至 laravel-tips 項目。