摘要:意味著依賴被注入進構造函數或者方法如果需要復用實例,可以定義為單例可以用接口或任何名稱來代替具體類。技能重寫構造函數參數方法允許將附加參數傳遞給構造函數。
本文大部分翻譯自 DAVE JAMES MILLER 的 《Laravel’s Dependency Injection Container in Depth》 。
上文介紹了 Dependency Injection Containers (容器) 的基本概念,現在接著深入講解 Laravel 的 Container。
Laravel 中實現的 Inversion of Control (IoC) / Dependency Injection (DI) Container 非常強悍,但文檔中很低調的沒有細講它。
準備工作 1.Dependency Injection本文中示例基于 Laravel 5.5 ,其它版本差不多。
關于 DI 請看這篇 《Laravel Dependency Injection (依賴注入) 概念詳解》,這里不再贅述。
2. 初識 ContainerLaravel 中有一大堆訪問 Container 實例的姿勢,比如最簡單的:
$container = app();
但我們還是先關注下 Container 類本身。
3. 在 Laravel 之外使用 IlluminateContainerLaravel 官方文檔中一般使用 $this->app 代替 $container。它是 Application 類的實例,而 Application 類繼承自 Container 類。
如果在 Laravel 之外
mkdir container && cd container composer require illuminate/container
// 新建一個 container.php,文件名隨便取 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 IlluminateContainerContainer; $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 IlluminateContainerContainer; $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(GitHubClient::class, function (Container $container) { $client = new GitHubClient; $client->setEnterpriseUrl(GITHUB_HOST); return $client; });技能T:Resolving Callbacks (回調)可用 resolveing() 方法來注冊一個 callback (回調函數),而不是直接覆蓋掉之前的 綁定。 這個函數會在綁定的類解析完成之后調用。
$container->resolving(GitHubClient::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 :-("]);技能D:Contextual Bindings (上下文綁定)注意:這種方式不是 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");技能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(); // dannyRebinding 的更多信息可以看這兩個鏈接:
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; });技能J:Overriding Constructor Parameters (重寫構造函數參數)注意:這種方式不是 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]);技能K:其它注意:Laravel 5.3 及以下使用 make($class, $parameters)。Laravel 5.4 中移除了此方法,但是在 5.4.16 以后又重新加回來了 makeWith() 。
這涵蓋了我認為有用的所有方法,但僅僅是簡介,不然這篇文章就寫不完了。。。
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")); // falsebindIf()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)); // falsefactory()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();我也不知道它有啥用,因為返回的閉包沒帶回參數。。。
afterResolving()注意:這個方法不是 Container 接口` 的一部分,只有在它的實現類 Container 才有。
afterResolving() 方法作用與 resolving() 完全相同,不同之處是 調用 「resolving」回調之后再調用 「afterResolving」回調。
最后再附幾個
不知道什么時候會用到它。。。isShared() – 確定一個給定的類型是一個 singleton/instance
isAlias() – 確定給定的字符串是否是已注冊的 別名
hasMethodBinding() - 確定容器是否具有給定的 method binding
getBindings() - 取回所有已注冊綁定的原始數組
getAlias($abstract) - 獲取基礎類/綁定名稱的別名
forgetInstance($abstract) - 清除單個實例對象
forgetInstances() - 清除所有實例對象
flush() - 清除所有綁定和實例,有效地重置容器
setInstance() - 替換 getInstance() 使用的實例 (提示:使用 setInstance(null)來清除它,這樣下一次它將生成一個新的實例)注意:這些方法不是 Container 接口 的一部分,只有在它的實現類 Container 才有。
原創。 所有 Laravel 文章均已收錄至 laravel-tips 項目。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/30647.html
摘要:控制反轉容器控制反轉使依賴注入變得更加便捷。有瑕疵控制反轉容器是實現的控制翻轉容器的一種替代方案。容器的獨立使用即使沒有使用框架,我們仍然可以在項目中使用安裝組件來使用的控制反轉容器。在沒有給定任何信息的情況下,容器是無法實例化相關依賴的。 聲明:本文并非博主原創,而是來自對《Laravel 4 From Apprentice to Artisan》閱讀的翻譯和理解,當然也不是原汁原味...
摘要:劃下重點,服務容器是用于管理類的依賴和執行依賴注入的工具。類的實例化及其依賴的注入,完全由服務容器自動的去完成。 本文首發于 深入剖析 Laravel 服務容器,轉載請注明出處。喜歡的朋友不要吝嗇你們的贊同,謝謝。 之前在 深度挖掘 Laravel 生命周期 一文中,我們有去探究 Laravel 究竟是如何接收 HTTP 請求,又是如何生成響應并最終呈現給用戶的工作原理。 本章將帶領大...
摘要:本文一大半內容都是通過舉例來讓讀者去理解什么是控制反轉和依賴注入,通過理解這些概念,來更加深入。這種由外部負責其依賴需求的行為,我們可以稱其為控制反轉。工廠模式,依賴轉移當然,實現控制反轉的方法有幾種。 容器,字面上理解就是裝東西的東西。常見的變量、對象屬性等都可以算是容器。一個容器能夠裝什么,全部取決于你對該容器的定義。當然,有這樣一種容器,它存放的不是文本、數值,而是對象、對象的描...
摘要:工廠模式,依賴轉移當然,實現控制反轉的方法有幾種。其實我們稍微改造一下這個類,你就明白,工廠類的真正意義和價值了。雖然如此,工廠模式依舊十分優秀,并且適用于絕大多數情況。 此篇文章轉載自laravel-china,chongyi的文章https://laravel-china.org/top...原文地址: http://www.insp.top/learn-lar... ,轉載務必保...
摘要:原文地址下面是中文翻譯擁有強大的控制反轉依賴注入容器。單例在使用自動綁定和時,每次需要時都會創建一個新的實例或者調用閉包。 原文地址 Laravels Dependency Injection Container in Depth 下面是中文翻譯 Laravel擁有強大的控制反轉(IoC)/依賴注入(DI) 容器。不幸的是官方文檔并沒有涵蓋所有可用的功能,因此,我決定嘗試寫文檔為自...
閱讀 1438·2021-09-28 09:44
閱讀 2501·2021-09-28 09:36
閱讀 1144·2021-09-08 09:35
閱讀 1982·2019-08-29 13:50
閱讀 810·2019-08-29 13:29
閱讀 1130·2019-08-29 13:15
閱讀 1724·2019-08-29 13:00
閱讀 2988·2019-08-26 16:16