Laravel 服務容器

Laravel 服務容器

服務容器綁定形式

1. bind 簡單綁定
    $this->app->bind('HelpSpot\API', function ($app) {
        return new HelpSpot\API($app->make('HttpClient'));
    });

2. singleton 綁定一個單例
    $this->app->singleton('HelpSpot\API', function ($app) {
        return new HelpSpot\API($app->make('HttpClient'));
    });

3. instance 綁定實例
    $api = new HelpSpot\API(new HttpClient);
    $this->app->instance('HelpSpot\Api', $api);

服務容器綁定剖析

1. singleton 和 bind 的解析

public function singleton($abstract, $concrete = null)
{
    // 實際是經過bind來實現的,區別在於最後的一個默認參數
    $this->bind($abstract, $concrete, true);
}
public function bind($abstract, $concrete = null, $shared = false)
{
    // 移除之前的實例和別名
    $this->dropStaleInstances($abstract);

    if (is_null($concrete)) {
        $concrete = $abstract;
    }
    // 統一成匿名函數的形式,進行統一的調用。**注意:這是laravel裏面的按需加載的一種實現方式**
    if (! $concrete instanceof Closure) {
        $concrete = $this->getClosure($abstract, $concrete);    
    }

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

    // 若已經實例化過了,則從新進行構建
    if ($this->resolved($abstract)) {
        $this->rebound($abstract);
    }
}
protected function dropStaleInstances($abstract)
{
    unset($this->instances[$abstract], $this->aliases[$abstract]);
}
protected function getClosure($abstract, $concrete)
{
    // 參數僅用來區分調用容器裏面的方法,build和make後面進行講解
    return function ($container, $parameters = []) use ($abstract, $concrete) {
        $method = ($abstract == $concrete) ? 'build' : 'make';    

        return $container->$method($concrete, $parameters);
    };
}
public function resolved($abstract)
{
    if ($this->isAlias($abstract)) {
        $abstract = $this->getAlias($abstract);
    }

    return isset($this->resolved[$abstract]) ||
           isset($this->instances[$abstract]);
}

小結:singleton和bind綁定以後的結果是填充一個容器屬性$this->bindings,爲後期的服務解析提供數據,數組以下laravel

$this->bindings[$abstract] = [
     'concrete' => $concrete,    // 匿名函數,用來構建實例
     'shared' => $shared,        // 此參數則是用來實現單例的關鍵,也是singleton和bind的差異所在。後期服務解析時會經過此參數和上下文參數來肯定是否放入$this->instances數組裏,這個就是單例的本質。
]
2. instance 的解析

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

    unset($this->aliases[$abstract]);
    // instance方法的本質,將實例$instance注入到容器的instances屬性裏
    $this->instances[$abstract] = $instance;    

    if ($this->bound($abstract)) {
        $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);
}

小結:instance綁定以後的結果是填充一個容器屬性的數組$this->instances,爲後期的服務解析提供數據,數組以下api

$this->instances[$abstract] = Object(xxx)  // instances對應的是具體的實現
3. 總結  

本質上,服務容器的綁定就是將相應的代碼(實例、類名、匿名函數等)注入到服務容器相應的屬性裏。
這樣,就能夠經過容器的服務解析(make)來進行相應的操做。固然,通常狀況都是經過服務容器來自
動解決類之間的依賴關係的(類的反射)。

服務解析

  • make | makeWith 方法(makeWith能夠指定參數)數組

    $api = $this->app->make('HelpSpot\API');
  • resolve 全局函數(當不能使用$this->app實例時,本質上是仍是調用容器的make或makeWith)app

    $api = resolve('HelpSpot\API');
  • 自動注入(最重要)ide

    能夠在類的構造函數或方法中對依賴使用「類型提示」,依賴的類將會被容器自動進行解析,包括在控制
    器,事件監聽器,隊列任務,中間件等地方。事實上,這也是大部分類被容器解析的方式。

服務解析剖析

1. 代碼解析

public function make($abstract)
{
    return $this->resolve($abstract);
}
public function makeWith($abstract, array $parameters)
{
    return $this->resolve($abstract, $parameters);
}
protected function resolve($abstract, $parameters = [])
{
    // 參數數組入棧
    $this->with[] = $parameters;
    // 是否有參數或爲上下文形式綁定(when綁定),優先從全局別名 $this->aliases 裏面取出最終值,沒有則返回原值
    $needsContextualBuild = ! empty($parameters) || ! is_null(
        $this->getContextualConcrete($abstract = $this->getAlias($abstract))    
    );
    // 若存在且沒有上下文關係的實例,則直接返回。
    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
        return $this->instances[$abstract];        
    }

    $concrete = $this->getConcrete($abstract);
    // 此方法是經過反射循環處理依賴關係的核心
    if ($this->isBuildable($concrete, $abstract)) {
        $object = $this->build($concrete);                
    } else {
        $object = $this->make($concrete);
    }
    // $this->app->extend($abstract, Closure $closure)方法能夠指定$abstract在實例化後須要執行的額外操做,將在此時進行調用
    foreach ($this->getExtenders($abstract) as $extender) {
        $object = $extender($object, $this);            
    }
    // singleton只實例化一次的本質
    if ($this->isShared($abstract) && ! $needsContextualBuild) {
        $this->instances[$abstract] = $object;            
    }
    // 對對象執行額外的函數,相似全局函數等後續操做,被定義在$this->globalResolvingCallbacks、$this->resolvingCallbacks、$this->globalAfterResolvingCallbacks、$this->afterResolvingCallbacks數組中,能夠經過$this->app->resolving()或$this->app->afterResolving()方法進行綁定
    $this->fireResolvingCallbacks($abstract, $object);    
    // 處理完以後標記爲已解決
    $this->resolved[$abstract] = true;

    array_pop($this->with);

    return $object;
}

注意:上面操做的$abstract鍵不少都是經過$this->getAlias($abstract)處理過的。$this->aliases數組相似函數

$this->aliases['Illuminate\Foundation\Application'] = 'app';
    $this->aliases['Illuminate\Contracts\Container\Container'] = 'app';
    $this->aliases['Illuminate\Contracts\Foundation\Application'] = 'app';

因此經過$this->getAlias($abstract)獲取到的是相似'app'的別名;
從resolve方法中也能夠看出,make 時先嚐試將 abstract 轉換爲 alias,再從 instances 取,最後才取 bindings。ui

public function getAlias($abstract)
{
    // 不存在則原值返回
    if (! isset($this->aliases[$abstract])) {
        return $abstract;
    }

    if ($this->aliases[$abstract] === $abstract) {
        throw new LogicException("[{$abstract}] is aliased to itself.");
    }
    // 不然取別名的最終值,可能會出現鏈式別名。a=>b=>c=>d,a的別名是b,b的別名是c,c的別名是d,最終取d
    return $this->getAlias($this->aliases[$abstract]);
}
protected function getContextualConcrete($abstract)
{
    // 若存在上下文的concrete,則直接返回
    if (! is_null($binding = $this->findInContextualBindings($abstract))) {
        return $binding;
    }

    if (empty($this->abstractAliases[$abstract])) {
        return;
    }

    foreach ($this->abstractAliases[$abstract] as $alias) {
        if (! is_null($binding = $this->findInContextualBindings($alias))) {
            return $binding;
        }
    }
}
protected function findInContextualBindings($abstract)
{
    // 此數組的構建方式是由$this->app->when(x)->needs(y)->give(z)方式來構建的,能夠理解爲:當x須要y時給z
    if (isset($this->contextual[end($this->buildStack)][$abstract])) {
        return $this->contextual[end($this->buildStack)][$abstract];
    }
}
protected function getConcrete($abstract)
{
    // 優先取對應的上下文的concrete
    if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
        return $concrete;
    }
    // 再從以前的$this->bindings裏面取
    if (isset($this->bindings[$abstract])) {
        return $this->bindings[$abstract]['concrete'];    
    }

    return $abstract;
}
protected function isBuildable($concrete, $abstract)
{
    return $concrete === $abstract || $concrete instanceof Closure;
}
// 經過反射處理依賴關係並構建相應的實例的方法 
public function build($concrete)
{
    // 若是是匿名函數則直接調用並返回
    if ($concrete instanceof Closure) {
        return $concrete($this, end($this->with));
    }
    // 不然獲取concrete的反射類
    $reflector = new ReflectionClass($concrete);
    // 若不能實例化,拋異常
    if (! $reflector->isInstantiable()) {
        return $this->notInstantiable($concrete);
    }
    // 將全部依賴的要處理的$concrete入棧,待一個一個處理完以後出棧,處理完全部的concrete以後應該是個空數組
    $this->buildStack[] = $concrete;
    // 獲取構造器
    $constructor = $reflector->getConstructor();

    // 沒有構造器(即沒有依賴),直接建立實例並返回
    if (is_null($constructor)) {
        array_pop($this->buildStack);

        return new $concrete;
    }
    // 獲取構造器的依賴關係,即參數類型
    $dependencies = $constructor->getParameters();

    // 處理依賴關係,返回的是一個依次含有全部依賴的實例
    $instances = $this->resolveDependencies(
        $dependencies
    );
    // 處理完就出棧
    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;
        }

        $results[] = is_null($class = $dependency->getClass())
                        ? $this->resolvePrimitive($dependency)    //resolvePrimitive基本類型處理
                        : $this->resolveClass($dependency);        //resolveClass類類型處理
    }

    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 {
        return $this->make($parameter->getClass()->name);    // 遞歸解析各種之間的依賴關係
    }
    catch (BindingResolutionException $e) {
        if ($parameter->isOptional()) {
            return $parameter->getDefaultValue();
        }

        throw $e;
    }
}
2. 總結

服務解析,實際上就是委託服務容器經過反射來處理類之間的依賴關係,從而獲得相應的實例。但前提是容器裏
服務的註冊,因此,須要再服務解析以前,將全部須要的服務注入到服務容器裏。而laravel裏面的服務注入方
式是經過服務提供者來進行注入的,而後在將服務提供者註冊到容器裏,相應的服務便會注入到容器裏。

容器事件

每當服務容器解析一個對象時就會觸發一個事件。你能夠使用 resolving 方法監聽這個事件this

$this->app->resolving(function ($object, $app) {
    // 解析任何類型的對象時都會調用該方法...
});    
$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
    // 解析「HelpSpot\API」類型的對象時調用...
});    
對應於$this->app->make()調用的resolve()方法:$this->fireResolvingCallbacks($abstract, $object);
注意:若是是單例,則只在解析時會觸發一次

額外補充[參考](http://d.laravel-china.org/do...

  1. 綁定初始數據code

    有時,你的類不只須要注入類,還須要注入一些原始數據,如一個整數。此時,你能夠容易地經過情景綁定注入須要的任何值:中間件

    $this->app->when('AppHttpControllersUserController')
    ->needs('$variableName')
    ->give($value);

  2. 綁定接口至實現

    服務容器有一個強大的功能,就是將一個指定接口的實現綁定到接口上。例如,若是咱們有一個 EventPusher 接口和一個它的實現類 RedisEventPusher 。編寫完接口的 RedisEventPusher 實現類後,咱們就能夠在服務容器中像下面例子同樣註冊它:

    $this->app->bind(

    'App\Contracts\EventPusher',
    'App\Services\RedisEventPusher'

    );

  3. 情境綁定

    有時候,你可能有兩個類使用到相同的接口,但你但願每一個類都能注入不一樣的實現。例如,兩個控制器可能須要依賴不一樣的 IlluminateContractsFilesystemFilesystem 契約 的實現類。 Laravel 爲此定義了一種簡單、平滑的接口:

    use IlluminateSupportFacadesStorage;
    use AppHttpControllersPhotoController;
    use AppHttpControllersVideoController;
    use IlluminateContractsFilesystemFilesystem;

    $this->app->when(PhotoController::class)

    ->needs(Filesystem::class)
    ->give(function () {
          return Storage::disk('local');

    });

    $this->app->when(VideoController::class)

    ->needs(Filesystem::class)
    ->give(function () {
          return Storage::disk('s3');

    });

  4. 標記

    有時候,你可能須要解析某個「分類」下的全部綁定。例如,你正在構建一個報表的聚合器,它須要接受不一樣 Report 接口的實例。分別註冊了 Report 實例後,你能夠使用 tag 方法爲他們賦予一個標籤:

$this->app->bind('SpeedReport', function () {
        //
    });
    $this->app->bind('MemoryReport', function () {
        //
    });
    $this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');
    一旦服務被標記後,你能夠經過 tagged 方法輕鬆地將它們所有解析:    
    $this->app->bind('ReportAggregator', function ($app) {
        return new ReportAggregator($app->tagged('reports'));
    });
相關文章
相關標籤/搜索