本文大部分翻譯自 DAVE JAMES MILLER 的 《Laravel’s Dependency Injection Container in Depth》 。
上文介紹了 Dependency Injection Containers (容器)
的基本概念,現在接著深入講解 Laravel
的 Container
。
Laravel
中實現的 Inversion of Control (IoC) / Dependency Injection (DI) Container
非常強悍,但文件中很低調的沒有細講它。
本文中示例基於
Laravel 5.5
,其它版本差不多。
準備工作
Dependency Injection
關於 DI
請看這篇 《Laravel Dependency Injection (依賴注入) 概念詳解》 。
初識 Container
Laravel
中有一大堆訪問 Container
例項的姿勢,比如最簡單的:
$container = app();
但我們還是先關注下 Container
類本身。
Laravel
官方文件中一般使用$this->app
代替$container
。它是Application
類的例項,而Application
類繼承自Container
類。
在 Laravel 之外使用 Illuminate\Container
mkdir container && cd container
composer require illuminate/container
// 新建一個 container.php,檔名隨便取
<?php
include './vendor/autoload.php';
use Illuminate\Container\Container;
$container = Container::getInstance();
Container 的技能們
Q. 基本用法,用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!');
W. Binding Interfaces to Implementations (繫結介面到實現)
用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!
E:Binding Abstract & Concret Classes (繫結抽象類和具體類):
繫結還可以用在抽象類:
$container->bind(MyAbstract::class, MyConcreteClass::class);
或者繼承的類中:
$container->bind(MySQLDatabase::class, CustomMySQLDatabase::class);
R:自定義繫結
如果類需要一些附加的配置項,可以把 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;
});
T:Resolving Callbacks (回撥)
可用 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
才會用到。
Y:Extending a Class (擴充套件一個類)
使用 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);
U:單例
使用 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);
I:Arbitrary Binding Names (任意繫結名稱)
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);
O:儲存任何值
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'];
P:Dependency Injection for Functions & Methods (給函式或方法注入依賴)
除了給建構函式注入依賴,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]);
都可以注入。
A: 呼叫例項方法的快捷方式
使用 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);
S:Method Call Bindings (方法呼叫繫結)
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 裡可以看到它加了什麼以及為什麼引數被忽略。
D:Contextual Bindings (上下文繫結)
有時候你想在不同的地方給介面不同的實現。這裡有 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');
F:Binding Parameters to Primitives (繫結初始資料)
當有一個類不僅需要接受一個注入類,還需要注入一個基本值(比如整數)。
還可以通過將變數名稱 (而不是介面) 傳遞給 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'];
});
G: Tagging (標記)
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']);
H:Rebinding (重新繫結)
這個功能很少用到,可以跳過,僅供參考。
在繫結或例項被使用之後又發生了變化,將呼叫一個 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/389745...
https://code.tutsplus.com/tutorials/diggin...
還有一個 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 才有。
J:Overriding Constructor Parameters (重寫建構函式引數)
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()
。詳見PR:https://github.com/laravel/framework/pull/...
K:其它
這涵蓋了我認為有用的所有方法,但僅僅是簡介,不然這篇文章就寫不完了。。。
bound()
如果一個類/名稱已經被 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()
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()
如果一個類已經被解析,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()
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();
我也不知道它有啥用,因為返回的閉包沒帶回引數。。。
注意:這個方法不是 Container 介面 的一部分,只有在它的實現類 Container 才有。
afterResolving()
afterResolving()
方法作用與 resolving()
完全相同,不同之處是 呼叫 「resolving」回撥之後再呼叫 「afterResolving」回撥。
不知道什麼時候會用到它。。。
最後再附幾個
isShared()
– 確定一個給定的型別是一個 singleton/instanceisAlias()
– 確定給定的字串是否是已註冊的 別名
hasMethodBinding()
- 確定容器是否具有給定的 method binding
getBindings()
- 取回所有已註冊繫結的原始陣列getAlias($abstract)
- 獲取基礎類/繫結名稱的別名forgetInstance($abstract)
- 清除單個例項物件forgetInstances()
- 清除所有例項物件flush()
- 清除所有繫結和例項,有效地重置容器setInstance()
- 替換 getInstance()
使用的例項 (提示:使用 setInstance(null)來清除它,這樣下一次它將生成一個新的例項)
注意:這些方法不是 Container 介面 的一部分,只有在它的實現類 Container 才有。
本作品採用《CC 協議》,轉載必須註明作者和本文連結
原創。 所有 Laravel 文章均已收錄至 Github laravel-tips 專案。