1. 程式人生 > 實用技巧 >laravel核心Ioc容器

laravel核心Ioc容器

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類例項化時剩下的兩個方法