眾所周知,Laravel 控制反轉 (IoC) / 依賴注入 (DI) 的功能非常強大,遺憾的是, 官方檔案 并沒有詳細講解它的所有功能,所以我決定自己實踐一下,并整理成文,下面的代碼是基于 Laravel 5.4.26 的,其他版本可能會有所不同,
了解依賴注入
我在這里不會詳細講解依賴注入/控制反轉的原則 - 如果你對此還不是很了解,建議閱讀 Fabien Potencier (Symfony 框架的創始人)的 What is Dependency Injection? ,
訪問容器
通過 Laravel 訪問 Container 實體的方式有很多種,最簡單的就是呼叫輔助函式 app():
1 $container = app();
為了突出重點 Container 類,這里就不贅述其他方式了,
注意: 官方檔案中使用的是 $this->app 而不是 $container,
(* 在 Laravel 應用中,Application 實際上是 Container 的一個子類 ( 這也說明了輔助函式 app() 的由來 ),不過在這篇文章中我還是將重點講解 Container 類的方法,)
在 Laravel 之外使用 Illuminate\Container
想要不基于 Laravel 使用 Container,安裝 然后:
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('[email protected]', 'MySuperSecurePassword!');
系結介面與具體實作
通過 Container 類,我們可以輕松實作從介面到具體類到實體的程序,首先定義介面:
interface MyInterface { /* ... */ } interface AnotherInterface { /* ... */ }
宣告實作介面的具體類,具體類還可以依賴其他介面( 或者是像上個例子中的具體類 ):
dependency = $dependency;/n }/n}","classes":{"has":1}}" data-cke-widget-upcasted="1" data-cke-widget-keep-attr="0" data-widget="codeSnippet">class MyClass implements MyInterface
1 { 2 private $dependency; 3 4 public function __construct(AnotherInterface $dependency) 5 { 6 $this->dependency = $dependency; 7 } 8 }
dependency = $dependency;/n }/n}","classes":{"has":1}}" data-cke-widget-upcasted="1" data-cke-widget-keep-attr="0" data-widget="codeSnippet">
然后使用 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 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.
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/112841.html
標籤:PHP
下一篇:分享學習 PHP 原始碼的方法
