Laravel修煉:服務容器繫結與解析

自行腦補發表於2018-10-14

前言

  老實說,第一次老大讓我看laravel框架手冊的那天早上,我是很絕望的,因為真的沒接觸過,對我這種渣渣來說,laravel的入門門檻確實有點高了,但還是得硬著頭皮看下去(雖然到現在我還有很多沒看懂,也沒用過)。
  後面慢慢根據公司專案的程式碼對laravel也慢慢熟悉起來了,但還是停留在一些表面的功能,例如依賴注入,ORM操作,使用者認證這些和我專案業務邏輯相關的操作,然後對於一些架構基礎的,例如服務提供器,服務容器,中介軟體,Redis等這些一開始就要設定好的東西,我倒是沒實際操作過(因為老大一開始就做好了),所以看手冊還是有點懵。
  所以有空的時候逛逛論壇,搜下Google就發現許多關於laravel核心架構的介紹,以及如何使用的網站(確實看完後再去看手冊就好理解多了),下面就根據一個我覺得不錯的網站上面的教學來記錄一下laravel核心架構的學習
網站地址:https://laraweb.net/ 這是一個日本的網站,我覺得挺適合新手的,內容用瀏覽器翻譯過來就ok了,畢竟日文直翻過來很好理解的

關於服務容器

  手冊上是這樣介紹的:Laravel 服務容器是用於管理類的依賴和執行依賴注入的工具。依賴注入這個花俏名詞實質上是指:類的依賴項通過建構函式,或者某些情況下通過「setter」方法「注入」到類中。。。。。。(真的看不懂啥意思)
  服務容器是用於管理類(服務)的例項化的機制。直接看看服務容器怎麼用

  1.在服務容器中註冊類(bind)

$this->app->bind(`sender`,`MailSender`);
//$this->app成為服務容器。

  2.從服務容器生成類(make)

$sender = $this->app->make(`sender`);
//從服務容器($this->app)建立一個sender類。
在這種情況下,將返回MailSender的例項。

  這是服務容器最簡單的使用,下面是對服務容器的詳細介紹
(主要參考:https://www.cnblogs.com/lyzg/…

laravel容器基本認識

  一開始,index.php 檔案載入 Composer 生成定義的自動載入器,然後從 bootstrap/app.php 指令碼中檢索 Laravel 應用程式的例項。Laravel 本身採取的第一個動作是建立一個 application/ service container 的例項。

$app = new IlluminateFoundationApplication(
    dirname(__DIR__)
);

  這個檔案在每一次請求到達laravel框架都會執行,所建立的$app即是laravel框架的應用程式例項,它在整個請求生命週期都是唯一的。laravel提供了很多服務,包括認證,資料庫,快取,訊息佇列等等,$app作為一個容器管理工具,負責幾乎所有服務元件的例項化以及例項的生命週期管理。當需要一個服務類來完成某個功能的時候,僅需要通過容器解析出該型別的一個例項即可。從最終的使用方式來看,laravel容器對服務例項的管理主要包括以下幾個方面:

  • 服務的繫結與解析
  • 服務提供者的管理
  • 別名的作用
  • 依賴注入

先了解如何在程式碼中獲取到容器例項,再學習上面四個關鍵

如何在程式碼中獲取到容器例項

第一種是

$app = app();
//app這個輔助函式定義在vendorlaravelframeworksrcIlluminateFoundationhelper.php
裡面,,這個檔案定義了很多help函式,並且會通過composer自動載入到專案中。
所以,在參與http請求處理的任何程式碼位置都能夠訪問其中的函式,比如app()。

第二種是

Route::get(`/`, function () {
    dd(App::basePath());
    return ``;
});
//這個其實是用到Facade,中文直譯貌似叫門面,在config/app.php中,
有一節陣列aliases專門用來配置一些型別的別名,第一個就是`App` => IlluminateSupportFacadesApp::class,
具體的Google一下laravel有關門面的具體實現方式

第三種是

  在服務提供者裡面直接使用$this->app。服務提供者後面還會介紹,現在只是引入。因為服務提供者類都是由laravel容器例項化的,這些類都繼承自IlluminateSupportServiceProvider,它定義了一個例項屬性$app:

abstract class ServiceProvider
{
    protected $app;

  laravel在例項化服務提供者的時候,會把laravel容器例項注入到這個$app上面。所以我們在服務提供者裡面,始終能通過$this->$app訪問到laravel容器例項,而不需要再使用app()函式或者App Facade了。

如何理解服務繫結與解析

  淺義層面理解,容器既然用來儲存物件,那麼就要有一個物件存入跟物件取出的過程。這個物件存入跟物件取出的過程在laravel裡面稱為服務的繫結與解析。

app()->bind(`service`, `this is service1`);

app()->bind(`service2`, [
    `hi` => function(){
        //say hi
    }
]);

class Service {

}

app()->bind(`service3`, function(){
    return new Service();
});

  還有一個單例繫結singleton,是bind的一種特殊情況(第三個引數為true),繫結到容器的物件只會被解析一次,之後的呼叫都返回相同的例項

public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}

  在繫結的時候,我們可以直接繫結已經初始化好的資料(基本型別、陣列、物件例項),還可以用匿名函式來繫結。用匿名函式的好處在於,這個服務繫結到容器以後,並不會立即產生服務最終的物件,只有在這個服務解析的時候,匿名函式才會執行,此時才會產生這個服務對應的服務例項。
  實際上,當我們使用singleton,bind方法以及陣列形式,(這三個方法是後面要介紹的繫結的方法),進行服務繫結的時候,如果繫結的服務形式,不是一個匿名函式,也會在laravel內部用一個匿名函式包裝起來,這樣的話, 不輪繫結什麼內容,都能做到前面介紹的懶初始化的功能,這對於容器的效能是有好處的。這個可以從bind的原始碼中看到一些細節:

if (! $concrete instanceof Closure) {
    $concrete = $this->getClosure($abstract, $concrete);
}

看看bind的底層程式碼

public function bind($abstract, $concrete = null, $shared = false)

  第一個引數服務繫結名稱,第二個引數服務繫結的結果(也就是閉包,得到例項),第三個引數就表示這個服務是否在多次解析的時候,始終返回第一次解析出的例項(也就是單例繫結singleton)。

  服務繫結還可以通過陣列的方式:

app()[`service`] = function(){
    return new Service();
};

繫結大概就這些,接下來看解析,也就是取出來用

$service= app()->make(`service`);

  這個方法接收兩個引數,第一個是服務的繫結名稱和服務繫結名稱的別名,如果是別名,那麼就會根據服務繫結名稱的別名配置,找到最終的服務繫結名稱,然後進行解析;第二個引數是一個陣列,最終會傳遞給服務繫結產生的閉包。

看原始碼:

/**
 * Resolve the given type from the container.
 *
 * @param  string  $abstract
 * @param  array  $parameters
 * @return mixed
 */
public function make($abstract, array $parameters = [])
{
    return $this->resolve($abstract, $parameters);
}

/**
 * Resolve the given type from the container.
 *
 * @param  string  $abstract
 * @param  array  $parameters
 * @return mixed
 */
protected function resolve($abstract, $parameters = [])
{
    $abstract = $this->getAlias($abstract);

    $needsContextualBuild = ! empty($parameters) || ! is_null(
        $this->getContextualConcrete($abstract)
    );

    // If an instance of the type is currently being managed as a singleton we`ll
    // just return an existing instance instead of instantiating new instances
    // so the developer can keep using the same objects instance every time.
    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
        return $this->instances[$abstract];
    }

    $this->with[] = $parameters;

    $concrete = $this->getConcrete($abstract);

    // We`re ready to instantiate an instance of the concrete type registered for
    // the binding. This will instantiate the types, as well as resolve any of
    // its "nested" dependencies recursively until all have gotten resolved.
    if ($this->isBuildable($concrete, $abstract)) {
        $object = $this->build($concrete);
    } else {
        $object = $this->make($concrete);
    }

    // If we defined any extenders for this type, we`ll need to spin through them
    // and apply them to the object being built. This allows for the extension
    // of services, such as changing configuration or decorating the object.
    foreach ($this->getExtenders($abstract) as $extender) {
        $object = $extender($object, $this);
    }

    // If the requested type is registered as a singleton we`ll want to cache off
    // the instances in "memory" so we can return it later without creating an
    // entirely new instance of an object on each subsequent request for it.
    if ($this->isShared($abstract) && ! $needsContextualBuild) {
        $this->instances[$abstract] = $object;
    }

    $this->fireResolvingCallbacks($abstract, $object);

    // Before returning, we will also set the resolved flag to "true" and pop off
    // the parameter overrides for this build. After those two things are done
    // we will be ready to return back the fully constructed class instance.
    $this->resolved[$abstract] = true;

    array_pop($this->with);

    return $object;
}

第一步:

$needsContextualBuild = ! empty($parameters) || ! is_null(
    $this->getContextualConcrete($abstract)
);

  該方法主要是區分,解析的物件是否有引數,如果有引數,還需要對引數做進一步的分析,因為傳入的引數,也可能是依賴注入的,所以還需要對傳入的引數進行解析;這個後面再分析。

第二步:

if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
    return $this->instances[$abstract];
}

  如果是繫結的單例,並且不需要上面的引數依賴。我們就可以直接返回 $this->instances[$abstract]。

第三步:

$concrete = $this->getConcrete($abstract);

...

/**
 * Get the concrete type for a given abstract.
 *
 * @param  string  $abstract
 * @return mixed   $concrete
 */
protected function getConcrete($abstract)
{
    if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
        return $concrete;
    }

    // If we don`t have a registered resolver or concrete for the type, we`ll just
    // assume each type is a concrete name and will attempt to resolve it as is
    // since the container should be able to resolve concretes automatically.
    if (isset($this->bindings[$abstract])) {
        return $this->bindings[$abstract][`concrete`];
    }

    return $abstract;
}

  這一步主要是先從繫結的上下文找,是不是可以找到繫結類;如果沒有,則再從 $bindings[] 中找關聯的實現類;最後還沒有找到的話,就直接返回 $abstract 本身。

// We`re ready to instantiate an instance of the concrete type registered for
// the binding. This will instantiate the types, as well as resolve any of
// its "nested" dependencies recursively until all have gotten resolved.
if ($this->isBuildable($concrete, $abstract)) {
    $object = $this->build($concrete);
} else {
    $object = $this->make($concrete);
}

...

/**
 * Determine if the given concrete is buildable.
 *
 * @param  mixed   $concrete
 * @param  string  $abstract
 * @return bool
 */
protected function isBuildable($concrete, $abstract)
{
    return $concrete === $abstract || $concrete instanceof Closure;
}

  如果之前找到的 $concrete 返回的是 $abstract 值,或者 $concrete 是個閉包,則執行 $this->build($concrete),否則,表示存在巢狀依賴的情況,則採用遞迴的方法執行 $this->make($concrete),直到所有的都解析完為止。

$this->build($concrete)

/**
 * Instantiate a concrete instance of the given type.
 *
 * @param  string  $concrete
 * @return mixed
 *
 * @throws IlluminateContractsContainerBindingResolutionException
 */
public function build($concrete)
{
    // If the concrete type is actually a Closure, we will just execute it and
    // hand back the results of the functions, which allows functions to be
    // used as resolvers for more fine-tuned resolution of these objects.
    // 如果傳入的是閉包,則直接執行閉包函式,返回結果
    if ($concrete instanceof Closure) {
        return $concrete($this, $this->getLastParameterOverride());
    }

    // 利用反射機制,解析該類。
    $reflector = new ReflectionClass($concrete);

    // If the type is not instantiable, the developer is attempting to resolve
    // an abstract type such as an Interface of Abstract Class and there is
    // no binding registered for the abstractions so we need to bail out.
    if (! $reflector->isInstantiable()) {
        return $this->notInstantiable($concrete);
    }

    $this->buildStack[] = $concrete;

    // 獲取建構函式
    $constructor = $reflector->getConstructor();

    // If there are no constructors, that means there are no dependencies then
    // we can just resolve the instances of the objects right away, without
    // resolving any other types or dependencies out of these containers.
    // 如果沒有建構函式,則表明沒有傳入引數,也就意味著不需要做對應的上下文依賴解析。
    if (is_null($constructor)) {
        // 將 build 過程的內容 pop,然後直接構造物件輸出。
        array_pop($this->buildStack);

        return new $concrete;
    }

    // 獲取建構函式的引數
    $dependencies = $constructor->getParameters();

    // Once we have all the constructor`s parameters we can create each of the
    // dependency instances and then use the reflection instances to make a
    // new instance of this class, injecting the created dependencies in.
    // 解析出所有上下文依賴物件,帶入函式,構造物件輸出
    $instances = $this->resolveDependencies(
        $dependencies
    );

    array_pop($this->buildStack);

    return $reflector->newInstanceArgs($instances);
}

上面這一段有關解析make的介紹主要參考:
coding01:看 Laravel 原始碼瞭解 Container

  這一篇就主要學習laravel的服務容器以及它的繫結和解析,雖然目前能力無法對框架原始碼每一個地方都弄懂,但通過這幾篇優秀的文章,我將其進行整理結合,這過程讓我更加理解laravel的一些核心內容,起碼別人問起來我多多少少能說出一些,這就是進步。

  後面有關服務提供者,依賴注入,中介軟體等內容的學習將放在後續的部落格文章中,歡迎看看我的其他部落格文章:https://zgxxx.github.io/
  以上相關知識的引用已經註明出處,若有侵權,請聯絡我,感謝這些優秀文章的作者

相關文章