Laravel核心——Ioc服務容器源碼解析(服務器綁定)

服務容器的綁定

bind 綁定

歡迎關注個人博客:www.leoyang90.cnphp

bind 綁定是服務容器最經常使用的綁定方式,在 上一篇文章中咱們討論過,bind 的綁定有三種:數組

  • 綁定自身服務器

  • 綁定閉包閉包

  • 綁定接口app

今天,咱們這篇文章主要從源碼上講解 Ioc 服務容器是如何進行綁定的。ide

/**
* Register a binding with the container.
*
* @param string|array $abstract
* @param \Closure|string|null $concrete
* @param bool $shared
* @return void
*/
public function bind($abstract, $concrete = null, $shared = false)
{
  // If no concrete type was given, we will simply set the concrete type to the
  // abstract type. After that, the concrete type to be registered as shared
  // without being forced to state their classes in both of the parameters.
  $this->dropStaleInstances($abstract);

  if (is_null($concrete)) {
    $concrete = $abstract;
  }

  // If the factory is not a Closure, it means it is just a class name which is
  // bound into this container to the abstract type and we will just wrap it
  // up inside its own Closure to give us more convenience when extending.
  if (! $concrete instanceof Closure) {
    $concrete = $this->getClosure($abstract, $concrete);
  }

  $this->bindings[$abstract] = compact('concrete', 'shared');

  // If the abstract type was already resolved in this container we'll fire the
  // rebound listener so that any objects which have already gotten resolved
  // can have their copy of the object updated via the listener callbacks.
  if ($this->resolved($abstract)) {
    $this->rebound($abstract);
  }
}

從源碼中咱們能夠看出,服務器的綁定有以下幾個步驟:函數

  1. 去除原有註冊。去除當前綁定接口的原有實現單例對象,和原有的別名,爲實現綁定新的實現作準備。ui

  2. 加裝閉包。若是實現類不是閉包(綁定自身或者綁定接口),那麼就建立閉包,以實現 lazy 加載。this

  3. 註冊。將閉包函數和單例變量存入 bindings 數組中,以備解析時使用。code

  4. 回調。若是綁定的接口已經被解析過了,將會調用回調函數,對已經解析過的對象進行調整。

去除原有註冊

dropStaleInstances 用於去除當前接口原有的註冊和別名,這裏負責清除綁定的 aliases 和單例對象的 instances,bindings 後面再作修改:

protected function dropStaleInstances($abstract)
{
    unset($this->instances[$abstract], $this->aliases[$abstract]);
}

加裝閉包

getClosure 的做用是爲註冊的非閉包實現外加閉包,這樣作有兩個做用:

  • 延時加載

服務容器在 getClosure 中爲每一個綁定的類都包一層閉包,這樣服務容器就只有進行解析的時候閉包纔會真正進行運行,實現了 lazy 加載的功能。

  • 遞歸綁定

對於服務容器來講,綁定是能夠遞歸的,例如:

$app->bind(A::class,B::class);
$app->bind(B::class,C::class);
$app->bind(C::class,function(){
    return new C;
})

對於 A 類,咱們直接解析 A 能夠獲得 B 類,可是若是僅僅到此爲止,服務容器直接去用反射去建立 B 類的話,那麼就頗有可能建立失敗,由於 B 類頗有可能也是接口,B 接口綁定了其餘實現類,要知道接口是沒法實例化的。

所以服務容器須要遞歸地對 A 進行解析,這個就是 getClosure 的做用,它把全部可能會遞歸的綁定在閉包中都用 make 函數,這樣解析 make(A::class) 的時候獲得閉包 make(B::class),make(B::class) 的時候會獲得閉包 make(C::class),make(C::class) 終於能夠獲得真正的實現了。

對於自我綁定的狀況,由於不存在遞歸狀況,因此在閉包中會使用 build 函數直接建立對象。(若是仍然使用 make,那就無限循環了)

protected function getClosure($abstract, $concrete)
{
    return function ($container, $parameters = []) use ($abstract, $concrete) {
        if ($abstract == $concrete) {
            return $container->build($concrete);
        }
        return $container->makeWith($concrete, $parameters);
    };
}

註冊

註冊就是向 binding 數組中添加註冊的接口與它的實現,其中 compact() 函數建立包含變量名和它們的值的數組,建立後的結果爲:

$bindings[$abstract] = [
  'concrete' => $concrete,
  'shared' => $shared
]

回調

註冊以後,還要查看當前註冊的接口是否已經被實例化,若是已經被服務容器實例化過,那麼就要調用回調函數。(若存在回調函數)
resolved() 函數用於判斷當前接口是否曾被解析過,在判斷以前,先獲取了接口的最終服務名:

public function resolved($abstract)
{
    if ($this->isAlias($abstract)) {
        $abstract = $this->getAlias($abstract);
    }

    return isset($this->resolved[$abstract]) ||
        isset($this->instances[$abstract]);
}
    
public function isAlias($name)
{
    return isset($this->aliases[$name]);
}

getAlias() 函數利用遞歸的方法獲取別名的最終服務名稱:

public function getAlias($abstract)
{
    if (! isset($this->aliases[$abstract])) {
        return $abstract;
    }

    if ($this->aliases[$abstract] === $abstract) {
        throw new LogicException("[{$abstract}] is aliased to itself.");
    }

    return $this->getAlias($this->aliases[$abstract]);
}

若是當前接口已經被解析過了,那麼就要運行回調函數:

protected function rebound($abstract)
{
    $instance = $this->make($abstract);

    foreach ($this->getReboundCallbacks($abstract) as $callback) {
        call_user_func($callback, $this, $instance);
    }
}
    
protected function getReboundCallbacks($abstract)
{
    if (isset($this->reboundCallbacks[$abstract])) {
        return $this->reboundCallbacks[$abstract];
    }

    return [];
}

這裏面的 reboundCallbacks 從哪裏來呢?這就是 Laravel核心——Ioc服務容器 文章中提到的 rebinding

public function rebinding($abstract, Closure $callback)
{
    $this->reboundCallbacks[$abstract = $this->getAlias($abstract)][] = $callback;
   
    if ($this->bound($abstract)) {
        return $this->make($abstract);
    }
}

值得注意的是: rebinding 函數不只綁定了回調函數,同時順帶還對接口abstract進行了解析,由於只有解析過,下次註冊纔會調用回調函數。

singleton 綁定

singleton 綁定僅僅是 bind 綁定的一個 shared 爲真的形式:

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

instance 綁定

不對接口進行解析,直接給接口一個實例做爲單例對象。從下面能夠看出,主要的工做就是去除接口在abstractAliases 數組和 aliases 數組中的痕跡,防止 make 函數根據別名繼續解析下去出現錯誤。若是當前接口曾經註冊過,那麼就調用回調函數。

public function instance($abstract, $instance)
{
    $this->removeAbstractAlias($abstract);

    $isBound = $this->bound($abstract);
     
    unset($this->aliases[$abstract]); 
           
    $this->instances[$abstract] = $instance;
        
    if ($isBound) {
        $this->rebound($abstract);
    }
}

protected function removeAbstractAlias($searched)
{
    if (! isset($this->aliases[$searched])) {
       return;
    }

    foreach ($this->abstractAliases as $abstract => $aliases) {
        foreach ($aliases as $index => $alias) {
            if ($alias == $searched) {
                unset($this->abstractAliases[$abstract][$index]);
            }
        }
    }
}

public function bound($abstract)
{
    return isset($this->bindings[$abstract]) ||
           isset($this->instances[$abstract]) ||
           $this->isAlias($abstract);
}

Context 綁定

Context 綁定通常用於依賴注入,當咱們利用依賴注入來自動實例化對象時,服務容器實際上是利用反射機制來爲構造函數實例化它的參數,這個過程當中,被實例化的對象就是下面的 concrete,構造函數的參數接口是 abstract,參數接口實際的實現是 implementation。
例如:

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('local');
          });

這裏實例化對象 concrete 就是 PhotoController,構造函數的參數接口 abstract 就是 Filesystem。參數接口實際實現 implementation 是 Storage::disk('local')。

這樣,每次進行解析構造函數的參數接口的時候,都會去判斷當前的 contextual 數組裏面 concrete[concrete] [abstract](也就是 concrete[PhotoController::class] [Filesystem::class])對應的上下文綁定,若是有就直接從數組中取出來,若是沒有就按照正常方式解析。
值得注意的是,concrete 和 abstract 都是利用 getAlias 函數,保證最後拿到的不是別名。

public function when($concrete)
{
    return new ContextualBindingBuilder($this, $this->getAlias($concrete));
}
public function __construct(Container $container, $concrete)
{
    $this->concrete = $concrete;
    $this->container = $container;
}
public function needs($abstract)
{
    $this->needs = $abstract;

    return $this;
}
public function give($implementation)
{
    $this->container->addContextualBinding(
        $this->concrete, $this->needs, $implementation
    );
}
public function addContextualBinding($concrete, $abstract, $implementation)
{
    $this->contextual[$concrete][$this->getAlias($abstract)] = $implementation;
}

tag 綁定

標籤綁定比較簡單,綁定過程就是將標籤和接口之間創建一個對應數組,在解析的過程當中,按照標籤把全部接口都解析一遍便可。

public function tag($abstracts, $tags)
{
    $tags = is_array($tags) ? $tags : array_slice(func_get_args(), 1);

    foreach ($tags as $tag) {
        if (! isset($this->tags[$tag])) {
            $this->tags[$tag] = [];
        }

        foreach ((array) $abstracts as $abstract) {
            $this->tags[$tag][] = $abstract;
        }
    }
}

數組綁定

利用數組進行綁定的時候 ($app()[A::class] = B::class),服務容器會調用 offsetSet 函數:

public function offsetSet($key, $value)
{
    $this->bind($key, $value instanceof Closure ? $value : function () use ($value) {
            return $value;
    });
}

extend擴展

extend 擴展分爲兩種,一種是針對instance註冊的對象,這種狀況將當即起做用,並更新以前實例化的對象;另外一種狀況是非 instance 註冊的對象,那麼閉包函數將會被放入 extenders 數組中,將在下一次實例化對象的時候才起做用:

public function extend($abstract, Closure $closure)
{
    $abstract = $this->getAlias($abstract);

    if (isset($this->instances[$abstract])) {
        $this->instances[$abstract] = $closure($this->instances[$abstract], $this);

        $this->rebound($abstract);
    } else {
        $this->extenders[$abstract][] = $closure;
        
        if ($this->resolved()) {
            $this->rebound($abstract);
        }
    }
}

服務器事件

服務器的事件註冊依靠 resolving 函數和 afterResolving 函數,這兩個函數維護着 globalResolvingCallbacks、resolvingCallbacks、globalAfterResolvingCallbacks、afterResolvingCallbacks 數組,這些數組中存放着事件的回調閉包函數,每當對對象進行解析時就會遍歷這些數組,觸發事件:

public function resolving($abstract, Closure $callback = null)
{
    if (is_string($abstract)) {
        $abstract = $this->getAlias($abstract);
    }

    if (is_null($callback) && $abstract instanceof Closure) {
        $this->globalResolvingCallbacks[] = $abstract;
    } else {
        $this->resolvingCallbacks[$abstract][] = $callback;
    }
}

public function afterResolving($abstract, Closure $callback = null)
{
    if (is_string($abstract)) {
        $abstract = $this->getAlias($abstract);
    }

    if ($abstract instanceof Closure && is_null($callback)) {
        $this->globalAfterResolvingCallbacks[] = $abstract;
    } else {
        $this->afterResolvingCallbacks[$abstract][] = $callback;
    }
}

Written with StackEdit.

相關文章
相關標籤/搜索