laravel核心概念總結

oliver-l發表於2021-02-22

這篇部落格在草稿箱堆了挺久的,原本還想趁著過年期間寫完這篇部落格的,後來又沒寫成,不過還是趁著上班後的幾天寫好了,想著使用laravel希望能多瞭解其內部原始碼的實現原理,所以結合文件和原始碼寫了這篇部落格。以下內容如果有錯歡迎指出。

服務容器

服務容器的概念理解可以檢視這裡我寫的服務容器相關概念。

結合文件和原始碼來分析一下laravel是如何實現的

根據文件的介紹,Laravel 服務容器是一個用於管理類依賴以及實現依賴注入的強有力工具。而服務容器正是laravel實現功能的核心利器。

laravel的服務容器提供了幾個比較核心的方法

  • singleton($abstract, $concrete = null)
  • bind($abstract, $concrete = null, $shared = false)
  • make($abstract, array $parameters = [])

實際上singleton和bind呼叫的函式實現是相同的,只是在呼叫引數上,將$shared變數置為true,通過$shared來判斷是否為單例物件

/**
* 新增單例類到容器中
* @param  string  $abstract
* @param  \Closure|string|null  $concrete
* @return void
*/
public function singleton($abstract, $concrete = null)
{
    $this->bind($abstract, $concrete, true);
}

檢視bind()方法,根據文件所給出的內容,bind 方法的第一個引數為要繫結的類 / 介面名,第二個引數是一個返回類例項的 Closure

    /**
     * 新增繫結類到容器中
     * @param  string  $abstract
     * @param  \Closure|string|null  $concrete
     * @param  bool  $shared
     * @return void
     */
    public function bind($abstract, $concrete = null, $shared = false)
    {
        //刪除容器例項和別名中$abstract的值
        $this->dropStaleInstances($abstract);

        //判斷是否為空,若為空則等於$abstract,後續容器可通過反射類獲取類例項
        if (is_null($concrete)) {
            $concrete = $abstract;
        }

        //判斷$concrete是否為閉包,如果不為閉包,則轉換成閉包的形式
        if (! $concrete instanceof Closure) {
            if (! is_string($concrete)) {
                throw new \TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
            }
            //將$concrete以閉包形式返回,這裡呼叫反射類獲取類例項
            $concrete = $this->getClosure($abstract, $concrete);
        }
        //將當前$abstract以key=>value的形式繫結到$this->binding變數中,其中$concrete為返回類例項,$shared用於判斷是否為單例物件
        $this->bindings[$abstract] = compact('concrete', 'shared');

        //判斷當前例項是否已存在,判斷是否需要回撥重新更新類例項
        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }

再來看看make()方法,從容器中解析出類例項,也是容器中比較核心的概念,其本質是呼叫了resolve方法實現,可檢視文件make方法

    /**
     * 從容器中解析當前例項
     * @param  string  $abstract
     * @param  array  $parameters
     * @return mixed
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    public function make($abstract, array $parameters = [])
    {
        return $this->resolve($abstract, $parameters);
    }

以下為resolve的原始碼實現

    /**
     * 解析指定類到容器中
     * @param  string  $abstract
     * @param  array  $parameters
     * @param  bool  $raiseEvents
     * @return mixed
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    protected function resolve($abstract, $parameters = [], $raiseEvents = true)
    {
        //檢視$abstract是否為別名,如果是則返回真正的類名
        $abstract = $this->getAlias($abstract);
        //獲取返回類例項
        $concrete = $this->getContextualConcrete($abstract);
        //
        $needsContextualBuild = ! empty($parameters) || ! is_null($concrete);

        //判斷單例陣列中是否已例項過,若已例項過則直接返回,這樣在整個框架的生命週期中,單例物件永遠只有一個
        if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];
        }

        $this->with[] = $parameters;
        //判斷類例項是否為空,為空則檢視$this->bingding繫結物件中是否存在例項,若沒有則等於$abstract類名
        if (is_null($concrete)) {
            $concrete = $this->getConcrete($abstract);
        }

        //判斷當前類是否可被解析,可以則呼叫build()方法對閉包或類名進行解析,不可解析則回撥make()方法,其中build()方法使用反射機制是實現框架依賴注入的核心
        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

        //遍歷定義的擴充套件,若已定義擴充套件則例項返回擴充套件閉包,這裡應該是為了方便後續更改某些類的實現,所以在這裡遍歷擴充套件
        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }

        //判斷是否為單例物件,為單例則將例項新增到$this->instances例項物件中
        if ($this->isShared($abstract) && ! $needsContextualBuild) {
            $this->instances[$abstract] = $object;
        }

        if ($raiseEvents) {
            $this->fireResolvingCallbacks($abstract, $object);
        }

        //將$this->resolved中的類名置為true,表示已解析完成
        $this->resolved[$abstract] = true;

        array_pop($this->with);
        //返回例項
        return $object;
    }

最後再來看看依賴注入的核心build()方法

    /**
     * 例項化具體物件類
     * @param  \Closure|string  $concrete
     * @return mixed
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    public function build($concrete)
    {
        //在上述講解,$concrete的值是一個閉包或者類名,這裡首先判斷值是否為閉包,若為閉包則直接執行,其中$this->getLastParameterOverride()方法用於獲取閉包對應引數
        if ($concrete instanceof Closure) {
            return $concrete($this, $this->getLastParameterOverride());
        }
        //生成$concrete的反射類,若類不存在則丟擲異常
        try {
            $reflector = new ReflectionClass($concrete);
        } catch (ReflectionException $e) {
            throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
        }

        //isInstantiable()為反射類中的函式,用於判斷類是否可以被例項化,若類為介面類和抽象類則不能被例項化,則丟擲異常
        if (! $reflector->isInstantiable()) {
            return $this->notInstantiable($concrete);
        }

        $this->buildStack[] = $concrete;
        //獲取類的建構函式
        $constructor = $reflector->getConstructor();

        //判斷建構函式是否為空,若為空則說明該類沒有依賴引數,則直接例項化返回
        if (is_null($constructor)) {
            array_pop($this->buildStack);

            return new $concrete;
        }
        //獲取建構函式引數
        $dependencies = $constructor->getParameters();

        //解析獲取類依賴引數,若解析失敗則丟擲異常
        try {
            $instances = $this->resolveDependencies($dependencies);
        } catch (BindingResolutionException $e) {
            array_pop($this->buildStack);

            throw $e;
        }

        array_pop($this->buildStack);
        //通過newInstanceArgs()建立類例項並返回
        return $reflector->newInstanceArgs($instances);
    }

服務提供者

根據文件所講解的,服務提供者是所有 Laravel 應用程式的引導中心。你的應用程式,以及 通過伺服器引導的 Laravel 核心服務都是通過服務提供器引導。但是,「引導」是什麼意思呢? 通常,我們可以理解為註冊,比如註冊服務容器繫結,事件監聽器,中介軟體,甚至是路由。服務提供者是配置應用程式的中心。

config/app.php檔案中,我們可以檢視到所有服務提供者

laravel核心概念總結

這裡隨便檢視一個服務提供者,如以下RedisServiceProvider為使用redis操作的類,根據文件定義我們只需要在register()中定義singleton(),bind()將例項註冊到容器中,在這裡是將redis操作類繫結為單例物件,redis連線類簡單繫結到容器中。同時RedisServiceProvider繼承了\Illuminate\Contracts\Support\DeferrableProvider介面並實現provides(),使服務可以被延時載入,註冊繫結的服務只有真的需要時才會被載入

laravel核心概念總結

深入分析服務提供者,檢視原始碼是如何實現的

    /**
     * 將配置中的服務提供者註冊到容器中
     * @return void
     */
    public function registerConfiguredProviders()
    {
        //用於獲取config/app.php中providers變數中的所有服務提供者
        $providers = Collection::make($this->config['app.providers'])
                        ->partition(function ($provider) {
                            return strpos($provider, 'Illuminate\\') === 0;
                        });
        $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);
        //載入註冊服務提供者
        (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
                    ->load($providers->collapse()->toArray());
    }

register()為註冊伺服器,將服務提供者中繫結的服務新增到容器中

    /**
     * 將單個服務提供者中的服務註冊到容器中
     * @param  \Illuminate\Support\ServiceProvider|string  $provider
     * @param  bool  $force
     * @return \Illuminate\Support\ServiceProvider
     */
    public function register($provider, $force = false)
    {
        //檢查該服務提供者是否已被載入過,若載入過則直接返回,如果$force為true則該服務提供者會重新載入
        if (($registered = $this->getProvider($provider)) && ! $force) {
            return $registered;
        }

        //判斷如果傳入的$provider變數為字串,則例項化服務提供者類
        if (is_string($provider)) {
            $provider = $this->resolveProvider($provider);
        }
        //執行服務提供者的register(),實際上就是註冊繫結服務到容器中
        $provider->register();

        //檢查類中是否定義$bindings變數,如果定義則將其繫結到容器中,可檢視文件https://learnku.com/docs/laravel/7.x/providers/7455#311d29
        if (property_exists($provider, 'bindings')) {
            foreach ($provider->bindings as $key => $value) {
                $this->bind($key, $value);
            }
        }
        //檢查類中是否定義$singletons變數,如果定義則將其繫結到容器中,可檢視文件https://learnku.com/docs/laravel/7.x/providers/7455#311d29
        if (property_exists($provider, 'singletons')) {
            foreach ($provider->singletons as $key => $value) {
                $this->singleton($key, $value);
            }
        }
        //將該服務提供者標記為已載入
        $this->markAsRegistered($provider);

        //檢查服務提供者是否定義boot()方法,若定義則會被執行,可檢視文件https://learnku.com/docs/laravel/7.x/providers/7455#ea148f
        if ($this->isBooted()) {
            $this->bootProvider($provider);
        }
        //返回服務提供者例項
        return $provider;
    }

Facades(門面)

根據文件描述Facades 為應用的 服務容器 提供了一個「靜態」 介面。Laravel 自帶了很多 Facades,可以訪問絕大部分功能。Laravel Facades 實際是服務容器中底層類的 「靜態代理」 ,相對於傳統靜態方法,在使用時能夠提供更加靈活、更加易於測試、更加優雅的語法。

Facades流程如下所示:
laravel核心概念總結

以Log門面類為例,檢視其程式碼,程式碼只定義了getFacadeAccessor()方法返回log字串,實際上我們使用Log日誌門面通常為Log::info()這樣的形式,當呼叫靜態方法時會觸發__callStatic魔術方法

laravel核心概念總結

    public static function __callStatic($method, $args)
    {
        //獲取容器中指定的例項,如上述為log,則返回容器中繫結例項中log的例項
        $instance = static::getFacadeRoot();

        if (! $instance) {
            throw new RuntimeException('A facade root has not been set.');
        }
        //呼叫例項中指定方法
        return $instance->$method(...$args);
    }

以上就是Facades(門面)的概念,也比較簡單,當然具體的內容也可以看文件Facades

Contracts(契約)

Contracts實際上就是定義一組介面,其理念符合基於介面而非實現程式設計的設計思想,這裡就沒什麼好說的了。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章