文章轉發自專業的Laravel開發者社區,原始連接:learnku.com/laravel/t/2…php
衆所周知,Laravel 控制反轉 (IoC) / 依賴注入 (DI) 的功能很是強大。遺憾的是, 官方文檔 並無詳細講解它的全部功能,因此我決定本身實踐一下,並整理成文。下面的代碼是基於 Laravel 5.4.26 的,其餘版本可能會有所不一樣。html
我在這裏不會詳細講解依賴注入/控制反轉的原則 - 若是你對此還不是很瞭解,建議閱讀 Fabien Potencier (Symfony 框架的創始人)的 What is Dependency Injection? 。laravel
經過 Laravel 訪問 Container 實例的方式有不少種,最簡單的就是調用輔助函數 app()
:git
$container = app();
複製代碼
爲了突出重點 Container 類,這裏就不贅述其餘方式了。github
注意: 官方文檔中使用的是 $this->app
而不是 $container
。數組
(* 在 Laravel 應用中,Application 其實是 Container 的一個子類 ( 這也說明了輔助函數 app()
的由來 ),不過在這篇文章中我仍是將重點講解 Container 類的方法。)緩存
想要不基於 Laravel 使用 Container,安裝 而後:bash
use Illuminate\Container\Container;
$container = Container::getInstance();
複製代碼
最簡單的用法是經過構造函數注入依賴類。session
class MyClass
{
private $dependency;
public function __construct(AnotherClass $dependency)
{
$this->dependency = $dependency;
}
}
複製代碼
使用 Container 的 make()
方法實例化 MyClass
類:閉包
$instance = $container->make(MyClass::class);
複製代碼
container 會自動實例化依賴類,因此上面代碼實現的功能就至關於:
$instance = new MyClass(new AnotherClass());
複製代碼
( 假設 AnotherClass
還有須要依賴的類 - 在這種狀況下,Container 會遞歸式地實例化全部的依賴。)
下面是一些基於 PHP-DI 文檔 的例子 - 將發送郵件與用戶註冊的代碼解耦:
class Mailer
{
public function mail($recipient, $content)
{
// 發送郵件
// ...
}
}
複製代碼
class UserManager
{
private $mailer;
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
public function register($email, $password)
{
// 建立用戶帳號
// ...
// 給用戶發送問候郵件
$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;
public function __construct(AnotherInterface $dependency)
{
$this->dependency = $dependency;
}
}
複製代碼
而後使用 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
),這自己在語法上就是錯誤的。
可更換的緩存層:
interface Cache
{
public function get($key);
public function put($key, $value);
}
複製代碼
class RedisCache implements 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();
複製代碼
也能夠與抽象類進行綁定:
$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 的實例做爲參數,若是須要的話,還能夠用它來實例化其餘類:
$container->bind(Logger::class, function (Container $container) {
$filesystem = $container->make(Filesystem::class);
return new FileLogger($filesystem, 'logs/error.log');
});
複製代碼
還能夠經過閉包函數自定義要如何實例化某個類:
$container->bind(GitHub\Client::class, function (Container $container) {
$client = new GitHub\Client;
$client->setEnterpriseUrl(GITHUB_HOST);
return $client;
});
複製代碼
可使用 resolving()
方法來註冊一個回調函數,當綁定被解析的時候,就調用這個回調函數:
$container->resolving(GitHub\Client::class, function ($client, Container $container) {
$client->setEnterpriseUrl(GITHUB_HOST);
});
複製代碼
全部的註冊的回調函數都會被調用。這種方法也適用於接口和抽象類:
$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);
複製代碼
還能夠註冊一個任何類被解析時都會被調用的回調函數 - 可是我想這可能僅適用於登陸和調試:
$container->resolving(function ($object, Container $container) {
// ...
});
複製代碼
你還可使用 extend()
方法把一個類與另外一個類的實例進行綁定:
$container->extend(APIClient::class, function ($client, Container $container) {
return new APIClientDecorator($client);
});
複製代碼
這裏返回的另一個類應該也實現了一樣的接口,不然會報錯。
只要使用 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);
複製代碼
其實,你可使用任意字符串做爲綁定的名稱,而不必定非要用類名或者接口名 - 可是這樣作的弊端就是不能使用類名實例化了,而只能使用 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 來存儲任何值 - 好比:配置數據:
$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');
複製代碼
除了依賴類,還能夠傳其餘參數:
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']);
// 或者:
$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]);
複製代碼
容器用於實例化類,這意味着:
例如,這將會啓做用:
class PostController
{
public function __construct(Request $request) { /* ... */ }
public function index(Cache $cache) { /* ... */ }
}
複製代碼
$container->singleton('post', PostController::class);
$container->call('post@index');
複製代碼
最後,你能夠將「默認方法」做爲第三個參數。若是第一個參數是一個沒有指定方法的類名,則將調用默認的方法。 Laravel 使用 事件處理 來實現:
$container->call(MyEventHandler::class, $parameters, 'handle');
// Equivalent to:
$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 :-(']);
複製代碼
注意: 這個方法不屬於 容器接口, 只是具體的 容器類. 參考 提交的 PR 瞭解爲何忽略參數。
有時候,你但願在不一樣的地方使用接口的不一樣實現。下面是來自 Laravel 文檔 中的一個例子:
$container
->when(PhotoController::class)
->needs(Filesystem::class)
->give(LocalFilesystem::class);
$container
->when(VideoController::class)
->needs(Filesystem::class)
->give(S3Filesystem::class);
複製代碼
如今, PhotoController 和 VideoController 均可以依賴於文件系統接口,可是每一個都將接收不一樣的實現。你還能夠爲 give()
使用閉包,就像使用 bind()
同樣:
$container
->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'];
});
複製代碼
你可使用容器 tag
來綁定相關標記:
$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']);
複製代碼
*Note: 這是一個更高級的,只是不多須要-請隨意跳過它! *
在綁定或實例已經被使用後須要更改時,能夠調用 rebinding()
回調 - 例如,此 Session
類在被 Auth
類使用後被替換,所以須要通知 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
複製代碼
還有一個快捷方法 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;
});
複製代碼
(就我的而言,我發現這種語法更加混亂,而且更喜歡上面更詳細的版本!)
Note: 這些方法不屬於 Container interface, 只有具體 Container class.
makeWith()
方法容許你將其餘參數傳遞給構造函數。 它忽略任何現有的實例或單例,而且在建立具備不一樣參數的類的多個實例時仍然有用,同時仍然注入依賴項:
class Post
{
public function __construct(Database $db, int $id) { /* ... */ }
}
複製代碼
$post1 = $container->makeWith(Post::class, ['id' => 1]);
$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語法.
這涵蓋了我認爲有用的全部方法 - 但只是爲了解決問題,這裏是剩下的公共方法的摘要......
若是類或名稱已與 bind()
, singleton()
, instance()
or 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()
作一樣的事情,除了它只註冊一個綁定(若是尚未)(參考上面的 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()
則返回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()
方法返回一個不帶參數的閉包,並調用 make()
。
$dbFactory = $container->factory(Database::class);
$db = $dbFactory();
複製代碼
我不肯定它有什麼用處...
wrap()
方法包裝一個閉包,以便在執行時注入它的依賴項。 wrap 方法接受一組參數, 返回的閉包沒有參數:
$cacheGetter = function (Cache $cache, $key) {
return $cache->get($key);
};
$usernameGetter = $container->wrap($cacheGetter, ['username']);
$username = $usernameGetter();
複製代碼
我不肯定它有什麼用處,由於閉包沒有參數...
Note: 這種方法不屬於 Container interface, 只屬於 Container class.
afterResolving()
方法與 resolving()
徹底相同,只是在「解析」回調以後調用 「解析後」 回調。 我不肯定何時會有用...
isShared()
- 肯定給定類型是否爲共享單例/實例isAlias()
- 肯定給定字符串是不是已註冊的別名hasMethodBinding()
- 肯定容器是否具備給定的方法綁定getBindings()
- 檢索全部已註冊綁定的原始數組getAlias($abstract)
- 解析基礎類/綁定名稱的別名forgetInstance($abstract)
- 清除單個實例對象forgetInstances()
- 清除全部實例對象flush()
- 清除全部綁定和實例,有效地重置容器setInstance()
- 替換 getInstance()
使用的實例(Tip:使用 setInstance(null)
清除它,因此下次它將生成一個新實例)Note: 最後一節中沒有一個方法是其中的一部分 Container interface.