# laravel Container (Service Container)(容器) https://mark-lin.com/posts/20181030/ https://learnku.com/articles/6158/laravel-container-container-understand-below https://blog.epoch.tw/2019/11/23/%E5%AD%B8%E7%BF%92-Laravel-Service-Container-%E6%9C%8D%E5%8B%99%E5%AE%B9%E5%99%A8/ 必看 https://laracasts.com/series/laravel-6-from-scratch/episodes/38 https://hackmd.io/@javck/ByJgF8HRP/%2FtBv0iPqSR1ms0NRsr8-HuQ 必看 https://iter01.com/327904.html 必看 https://segmentfault.com/q/1010000020654071 https://www.bilibili.com/video/BV1Np4y197Tu/?spm_id_from=333.788.recommend_more_video.11 都會再來看 https://www.cntofu.com/book/107/Laravel%20Container%E2%80%94%E2%80%94IoC%20%E6%9C%8D%E5%8A%A1%E5%AE%B9%E5%99%A8.md ## ArrayAccess https://devindeving.blogspot.com/2021/06/php-arrayaccess.html https://learnku.com/articles/5595/introduction-and-use-of-arrayaccess ## 手把手解析 https://learnku.com/docs/the-laravel-way/5.6/Tao-3-2/2929 ## 重點觀念區 簡單好懂 https://www.youtube.com/watch?v=5glsdzGeYQo 从数据类型本质上来说,Laravel 服务容器就是一个类,PHP 解析过程会被实例化为一个对象。我们都知道 PHP 实例化类要调用类的构造方法的,而这个执行构造方法的过程就是 服务容器的初始化。 ![](https://i.imgur.com/nYV6VTd.jpg) 然後他看不懂interface所以要註冊就這樣而已 在容器裡面在註冊 一樣是get set這樣 所以也是容器 註冊可以app()->bind只是太亂所以寫在servicePovider 然後因為povider會再啟動時候執行 所以就會綁定 你可以看到裡面有bind 跟 resolve 就像之前看解析container的get set一樣 从程序设计原则上看,Laravel 服务容器就是我们经常听到的 IoC/DI 容器,而且还是一个高级 IoC/DI 容器。 从程序设计模式上看,Laravel 服务容器可以看成 一棵树,一颗注册树。 laravel能直接用container class能直接用DI 沒有街口還能反射 的原因是 laravel在index那邊$app就註冊了container laravel本身就是個大個container ``` $app = require_once __DIR__.'/../bootstrap/app.php'; $kernel = $app->make(Kernel::class); ``` **外部服務、商業邏輯,把他拆成 Service** 其實就是class 只是單純做一件事情 就把它當成 servuie 依賴注入的意思就如同字面所說,類別所依賴的對象,比如物件參數是透過建構子或者是設定子這幾種方式來傳入而非自己產生 **多數時候,Dependency Injection 並不需要 Container。** **Laravel 官方文档中一般使用 $this->app 代替 $container。它是 Application 类的实例,而 Application 类继承自 Container 类。** 如果類別沒有依賴**任何的介面**,那麼就沒有將類別綁定至容器中的必要。並不需要告訴容器如何建構這些物件,因為它會透過 PHP 的 reflection 自動解析出物件。 **有用到在bind去make 不然直接用di注入就好 DI一般情況不用最好是嵌套情況再使用 ex controller使用serive在使用Repository** boot方法會在所有regis方法結束後 看有沒有boot才跑 所以能不寫就不寫 ## 訪問容器 Laravel中有幾種訪問Container例項的方法,但最簡單的方法是呼叫app()helper ![](https://i.imgur.com/mHX7sRD.jpg) 如果不用app 可以直接在建構子輸入 ## laravel 裡面的舉例 Laravel 服務容器(Container)是管理類別依賴與執行依賴注入的強力工具。依賴注入是個花俏的名詞,事實上是指:類別的依賴透過建構子「注入」,或在某些情況下透過「setter」方法注入。 一般不會用到,只有框架會用到 **在 Laravel 中所代表的意思就是指裡面裝了一堆可以用的服務載體,就叫 Container。** ## 為什麼要使用 Container ? 為什麼 Laravel 要使用 Container 呢,為什麼上面的要實體化 $knernel 時,不使用 new Knernel() 這種實體化的方式呢 ? **因為它想解決依賴與耦合。** ## app跟resolve ![](https://i.imgur.com/45sKlMJ.png) 一樣 ## 自己的看法 有共同clasee在繼承同樣的街口,減少依賴,但會變成`$log = new Log(new AWSLogServcie());` 這一段程式碼本身就依賴了Log這個類別,這樣事實上還是沒有解決依賴的問題,因此 Laravel 建立了 Container,並且會在開啟服務時,先行註冊好,就不會依賴了,感覺是有點全域的感覺,所以是大型專案才要考慮,不然麻煩,有Container就要註冊,然後使用,使用就用IOC,就不用make了 ## Laravel 如何建立 Container ? 這裡我們就要開始來研究一下 Laravel Container 的原始碼。 首先最一開始是這裡,它會實體化一個 $app conatiner。 ``` <?php $app = new Illuminate\Foundation\Application( $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) ); ``` 接下來我們來看一下 Illuminate\Foundation\Application 的程式碼。這裡可以知道 Application 繼承了 Container 這個類別。 ``` <?php class Application extends Container implements ApplicationContract, HttpKernelInterface { public function __construct($basePath = null) { if ($basePath) { $this->setBasePath($basePath); } $this->registerBaseBindings(); $this->registerBaseServiceProviders(); $this->registerCoreContainerAliases(); } } ``` ### 初始化 像我們每當要執行 Laravel 時,都會先執行下面這段程式碼,其中 app就是我們的Container,然後接下來會使用Container來實體化一些物件,例如kernel。 ``` <?php public/index.php $app = require_once __DIR__.'/../bootstrap/app.php'; /* |-------------------------------------------------------------------------- | Run The Application |-------------------------------------------------------------------------- | | Once we have the application, we can handle the incoming request | through the kernel, and send the associated response back to | the client's browser allowing them to enjoy the creative | and wonderful application we have prepared for them. | */ $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); $response = $kernel->handle( $request = Illuminate\Http\Request::capture() ); $response->send(); $kernel->terminate($request, $response); ``` ## contriner 實例化 laravel 中 app() 就是使用 Container 实例化的一个助手函数 ## 為什麼不用new 實體化的方式 **因為它想解決依賴與耦合。** 舉例 高依賴與耦合 : 程式碼中綁死了某個模組,如下面程式碼綁死了 Log Service ``` <?php class Log { public function send(log): void { $awsLogService = new AWSLogService(); $awsLogService->send(log); } } class AWSLogService { public function send(log): void { .... } } ``` 但假設今天我們要將 Log 改傳到 GCP ( Google 雲端 ),那我們程式碼要修改成如下 : ``` <?php class Log { public function send(log): void { //$awsLogService = new AWSLogService(); //$awsLogService->send(log); $gcpLogService = new GCPLogService(); $gcpLogService->send(log); } } class GCPLogService { public function send(log): void { .... } } // 使用 $log = new Log(); $log->send('log.....'); ``` 從上面程式碼中,我們可以注意到我們沒當要換個服務時,都需要修改程式碼,並且這裡還有一個缺點,你要如何做單元測試 ? 程式碼裡面完全的綁死了 AWSLogService 或是 GCPLogService,沒有地方可以給我們進行替換,沒辦法替換就代表我們在做測試時,只能真的將資料丟到 AWS 或 GCP。 **(低) 依賴與耦合***(改用implements ) 看文章 **build 和 make 都是在一个闭包函数中,闭包函数不触发,它是不会创建对象的。也就是所谓的懒加载** ## 簡易綁定 首先新增一個路由在 routes/web.php 檔。 `Route::get('pay', 'OrderController@store');` ## bind 建立抽象與實體的綁定表 bind 註冊:建立抽象與實體類別的綁定表。將 Interface 和實現他的 class 綁定 **bind主要是綁定給一個名字** 如果不綁名字 在你引入的時候除非用app() 用di的話bind要app\models這樣之類的 幫他設名字 ->bind('week') 這樣只能用app()解析 因為他是container的專門方法 ``` <?php // 1. 類別綁定 clouse //綁定叫UserRepository App::bind('UserRepository', function() { return new AWSUserRepository; }); // 2. 抽像類別綁定實際類別 //綁定叫UserRepository App::bind('UserRepositoryInterface', 'DbUserRepository'); // 3. 實際類別綁定 //綁定叫UserRepository APP::bind('UserRepository') // 4. singleton 綁定 //綁定叫UserRepository App::singleton('UserRepository', function() { return new AWSUserRepository; }); ``` ## make **產生實際的實體物件** **make 方法傳入我們剛才定義的變量名即可調用該服務** 用途:實例化。(用 interface 名,非 class 名)其執行順序為: resolve 解析 解析後若沒有其他依賴,則 build。若還有依賴,遞回 call make,繼續解析依賴 ![](https://i.imgur.com/uPoTWlR.png) ## 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; }); ``` ## 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->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); ``` ## Laravel新建对象的方法:make resolve 辅助函数app() https://blog.csdn.net/fujian9544/article/details/109029276 make方法 你可以使用 make 方法从容器中解析出类实例。 make 方法接收你想要解析的类或接口的名字: `$api = $this->app->make('HelpSpot\API');` resolve 方法: **resolve 函数使用服务容器将给定类或接口名解析为对应绑定实例:** 如果你的代码处于无法访问 $app 变量的位置,则可用全局辅助函数 resolve 来解析: $api = resolve('HelpSpot\API'); 如果类依赖不能通过容器解析,你可以通过将它们作为关联数组作为 makeWith 方法的参数注入: $api = $this->app->makeWith('HelpSpot\API', ['id' => 1]); 辅助函数app() $api = app('HelpSpot\API'); ## 容器事件 服務容器每次解析對象會觸發一個事件,你可以使用 resolving 方法監聽這個事件: use App\Services\Transistor; $this->app->resolving(Transistor::class, function ($api, $app) { // 當容器解析型別為 "Transistor::class" 的物件時被呼叫... }); $this->app->resolving(function ($object, $app) { // 當容器解析任何型別的物件時被呼叫... }); 正如你所看到的,被解析的對象將會被傳入回呼函數,這使得你能夠在對象被傳給調用者之前給它設置額外的屬性 ## 總結 有interface再用bind跟make , 不然直接DI就好 bind 綁定名字() make實現(用綁定名字) 註冊在ServiceProvider 的 registar 使用在config/app.php的 providers 記得技術長有說過不用bind也可以 直接抓他名字路徑 ## 實現container https://www.youtube.com/watch?v=y7EbrV4ChJs 因為你能抓它裡面全部的 所以應該是這樣的寫法 一層一層 ![](https://i.imgur.com/cigaC2p.jpg) 然後get這樣 ![](https://i.imgur.com/XAMlkIV.jpg) 都是一層一層的 但這樣不是container 這是他的核心 接下來就近接了 ### container進階版 autowiring 就是現在大家用的 沒有顯示綁定 所以上面那些遞迴都可以刪除了 這是基於反射的實現的 第一步建立class ![](https://i.imgur.com/xH632xa.png) 第二步建立他的依賴項 用反射get他的建構式所以上面那行可以去掉 ![](https://i.imgur.com/ZiNCEWz.png) 將每個parms都拉出來做事 ![](https://i.imgur.com/9wxLZkG.png) 抓取他的typeHint 跑遞迴 ![](https://i.imgur.com/utc4IjK.jpg) 最後get的時候 先判斷有沒有顯示綁定(就是typeHint 隱式指interFace) 然後new反射 在newInstanceArgs(剛剛寫的) 實例他的參數 ###### tags: `Laravel`