laravel核心Ioc容器
阿新 • • 發佈:2020-07-23
laravel容器和依賴注入
-
啥是Ioc容器,方便我們實現依賴注入的一種實現,也就是說依賴注入不一定需要控制反轉容器,只不過使用容器可能會方便些。
-
laravel通過向容器中繫結介面的具體實現,可實現不同實現的快速切換,介面在laravel中有個好聽的名字叫契約。
-
面向介面程式設計和容器結合使用,可以輕鬆實現程式碼解耦,實現了關注分離。
-
面向介面開發的好處:除了可以快速切換實現了相同契約的實現,對開發測試同步進行,以及對單元測試都是非常有好的。
-
下面是一個簡單的使用示例,為了相對好理解沒有加入service層。
1 建立文章介面 <?php namespace App\Repository; interface ArticleRepository { // 返回文章列表 public function getList(): array; } 2 建立一個文章具體實現類 <?php namespace App; use App\Repository\ArticleRepository; use App\Models\Article; class DbArticle implements ArticleRepository { public function getList(): array { return Article::all()->toArray(); } } 3 在容器中進行繫結 <?php namespace App\Providers; use App\DbArticle; use Illuminate\Support\ServiceProvider; use App\Repository\ArticleRepository; use App\DbArticle; class AppServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { $this->app->bind(ArticleRepository::class, function () { return new DbArticle(); }); } 4 在控制器中使用 <?php namespace App\Http\Controllers\Test; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use App\Repository\ArticleRepository; class ArticleController extends Controller { protected $articles; public function __construct(ArticleRepository $articles) { $this->articles = $articles; } public function index() { $articles = $this->articles->getList(); return view('article.index', compact('articles')); } } 以上便是一個比較標準的依賴注入的使用方式,其實還有很多使用方式,比如通過app()函式或者App門面直接獲取依賴等等。看到這裡應該能感覺到依賴注入為開發者帶來的方便了,上述例子中文章是從ORM中取出,當需求改變要求文章從mongodb或者redis中取出的時候,我們只需要編寫單獨的實現類,然後在服務提供者中繫結新的實現,業務程式碼完全不需要改變,就能快速實現切換。
-
下面講解laravel如何實現的依賴注入 laravel版本6.12
從bootstrap/app.php開始 // 例項化app容器類 並傳遞專案根目錄給建構函式 $app = new Illuminate\Foundation\Application( $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) ); Illuminate\Foundation\Application中 public function __construct($basePath = null) { if ($basePath) { // 註冊基本路徑 呼叫container的instance方法 新增路徑例項到容器 // 結果就是app例項的instances屬性下多了幾條路徑對映 可直接列印$this檢視 $this->setBasePath($basePath); } // 註冊基本繫結 跳轉到下面的registerBaseBindings方法 $this->registerBaseBindings(); $this->registerBaseServiceProviders(); $this->registerCoreContainerAliases(); } protected function registerBaseBindings() { static::$instance = $this; // 繫結自身到容器 $this->instance('app', $this); // 依然還是繫結 $this->instance(Container::class, $this); // 重點來了!!! 跳轉到下面的singleton方法 $this->singleton(Mix::class); $this->instance(PackageManifest::class, new PackageManifest( new Filesystem, $this->basePath(), $this->getCachedPackagesPath() )); } Illuminate\Container\Container中 // 繫結共享例項 就是單例繫結 可以看到singleton方法就是呼叫了bind方法 public function singleton($abstract, $concrete = null) { $this->bind($abstract, $concrete, true); } /** * Register a binding with the container. * * @param string $abstract * @param \Closure|string|null $concrete 注意引數型別 * @param bool $shared * @return void */ public function bind($abstract, $concrete = null, $shared = false) { // 此處的$abstract = Illuminate\Foundation\Mix // 刪除老的例項繫結 $this->dropStaleInstances($abstract); // laravel預設的如果沒傳遞對應抽象的例項,那麼就認為例項應該是傳遞的抽象的一個例項 if (is_null($concrete)) { // 如果concrete為null $concrete = $abstract; } // 如果傳遞的不是一個閉包 if (!$concrete instanceof Closure) { // laravel會試圖根據傳遞進來的抽象得到一個閉包 // 跳轉到getClosure方法 // $concrete = $this->getClosure($abstract, $concrete); } // 將返回的閉包和是否shared標誌儲存到bindings陣列下 $this->bindings[$abstract] = compact('concrete', 'shared'); if ($this->resolved($abstract)) { $this->rebound($abstract); } } /** * Get the Closure to be used when building a type. * * @param string $abstract * @param string $concrete * @return \Closure */ // 值得注意的是此時生成的閉包不會立即執行,觸發條件官方在註釋中寫的很清楚,當要使用時才會執行此閉包 protected function getClosure($abstract, $concrete) { // 此時的$abstract $concrete都是Illuminate\Foundation\Mix return function ($container, $parameters = []) use ($abstract, $concrete) { // 如果呼叫bind方法只傳入了abstract if ($abstract == $concrete) { // 返回容器的build方法返回的例項 return $container->build($concrete); } // 否則直接呼叫contaner的resolve方法進行解析 return $container->resolve( $concrete, $parameters, $raiseEvents = false ); }; } // 什麼時候使用到這些繫結呢(如何從容器中解析需要的例項呢) 來看make方法 呼叫方式app()->make($yourAbstract) /** * Resolve the given type from the container. * * @param string $abstract * @param array $parameters * @return mixed * * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function make($abstract, array $parameters = []) { // 跳轉到resolve方法 return $this->resolve($abstract, $parameters); } /** * Resolve the given type from the container. * * @param string $abstract * @param array $parameters * @param bool $raiseEvents * @return mixed * * @throws \Illuminate\Contracts\Container\BindingResolutionException */ protected function resolve($abstract, $parameters = [], $raiseEvents = true) { $abstract = $this->getAlias($abstract); $needsContextualBuild = !empty($parameters) || !is_null( $this->getContextualConcrete($abstract) ); // 如果要解析的類名instances中已經有了 並且不需要臨時build 那麼就返回已經存在的例項 if (isset($this->instances[$abstract]) && !$needsContextualBuild) { // dd(123, $abstract); // dd($this->instances); return $this->instances[$abstract]; } $this->with[] = $parameters; // getConcrete方法 拿到之前可能繫結過的binding 繫結過就是一個閉包 // 沒繫結過返回自身,即通過容器make可一個自定義的類 $concrete = $this->getConcrete($abstract); if ($this->isBuildable($concrete, $abstract)) { // 當concrete === abstract 或者 concrete是一個閉包的時候 直接呼叫build方法 // 跳轉到build方法 $object = $this->build($concrete); } else { // 當concrete是一個類名的時候 呼叫make方法 $object = $this->make($concrete); } foreach ($this->getExtenders($abstract) as $extender) { $object = $extender($object, $this); } // 如果是共享的例項 或者 不需要臨時build 那麼就將例項存放到容器的instances陣列中 if ($this->isShared($abstract) && !$needsContextualBuild) { $this->instances[$abstract] = $object; } if ($raiseEvents) { $this->fireResolvingCallbacks($abstract, $object); } $this->resolved[$abstract] = true; array_pop($this->with); // 返回例項 return $object; } public function build($concrete) { // 如果傳遞進來的是一個閉包 (通過之前bind方法繫結得到的閉包) // 直接呼叫閉包返回例項 if ($concrete instanceof Closure) { return $concrete($this, $this->getLastParameterOverride()); } // 如果傳遞進來的是一個可能的類名 那麼呼叫反射api解析依賴 try { $reflector = new ReflectionClass($concrete); } catch (ReflectionException $e) { throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e); } // 不能例項化就丟擲異常 if (!$reflector->isInstantiable()) { return $this->notInstantiable($concrete); } $this->buildStack[] = $concrete; // 拿到構造方法 $constructor = $reflector->getConstructor(); // 沒有構造方法直接new例項 if (is_null($constructor)) { array_pop($this->buildStack); return new $concrete; } // 拿到構造方法中的依賴 $dependencies = $constructor->getParameters(); try { // 解析依賴 // 跳轉到resolveDependencies方法 $instances = $this->resolveDependencies($dependencies); } catch (BindingResolutionException $e) { array_pop($this->buildStack); throw $e; } array_pop($this->buildStack); // 返回例項 return $reflector->newInstanceArgs($instances); } protected function resolveDependencies(array $dependencies) { $results = []; foreach ($dependencies as $dependency) { if ($this->hasParameterOverride($dependency)) { // 如果存在臨時重寫 則獲取 $results[] = $this->getParameterOverride($dependency); continue; } // 如果構造方法中不存在型別提示 那麼會返回null // 跳轉到resolvePrimitive方法 $results[] = is_null($dependency->getClass()) ? $this->resolvePrimitive($dependency) // 如果存在型別提示 根據型別嘗試解析出依賴例項 getClass能拿到一個確切的類名 // 跳轉到resolveClass方法 : $this->resolveClass($dependency); } return $results; } protected function resolvePrimitive(ReflectionParameter $parameter) { // 首先嚐試獲取閉包 if (!is_null($concrete = $this->getContextualConcrete('$' . $parameter->name))) { return $concrete instanceof Closure ? $concrete($this) : $concrete; } // 嘗試獲取預設值 if ($parameter->isDefaultValueAvailable()) { return $parameter->getDefaultValue(); } // 如果上面都不符合那麼就直接丟擲異常 $this->unresolvablePrimitive($parameter); } protected function resolveClass(ReflectionParameter $parameter) { try { // 又回到了make方法 進行了遞迴呼叫 // 至此一切形成了閉環 完美!!! return $this->make($parameter->getClass()->name); } catch (BindingResolutionException $e) { if ($parameter->isOptional()) { return $parameter->getDefaultValue(); } throw $e; } } 可以看到: 1 當從laravel中解析物件的時候,實際呼叫的是build方法,而build方法首先會嘗試從之前的繫結中獲取,如果未找到繫結那麼就通過反射機制嘗試解析,當然解析依賴的過程依然存在嘗試獲得之前繫結的過程。 2 laravel的容器解析依賴需要使用其固定的規則,直接呼叫app()函式或者App門面從容器中解析。 3 建議使用容器進行解析自定義類的時候,一定要加上依賴的型別 4 繫結的時候儘量使用閉包形式
其中的臨時繫結等需要配合laravel容器的when with等api使用,還有解析時的事件等,本文都沒進行講解。
下期預告:Application類例項化時剩下的兩個方法