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,为后期的服务解析提供数据,数组如下

$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,为后期的服务解析提供数据,数组如下

$this->instances[$abstract] = Object(xxx)  // instances对应的是具体的实现
3. 总结  

本质上,服务容器的绑定就是将相应的代码(实例、类名、匿名函数等)注入到服务容器相应的属性里。
这样,就可以通过容器的服务解析(make)来进行相应的操作。当然,一般情况都是通过服务容器来自
动解决类之间的依赖关系的(类的反射)。

服务解析

  • make | makeWith 方法(makeWith可以指定参数)

    $api = $this->app->make('HelpSpot\API');
    
  • resolve 全局函数(当不能使用$this->app实例时,本质上是还是调用容器的make或makeWith)

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

    可以在类的构造函数或方法中对依赖使用「类型提示」,依赖的类将会被容器自动进行解析,包括在控制
    器,事件监听器,队列任务,中间件等地方。事实上,这也是大部分类被容器解析的方式。
    

服务解析剖析

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。

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->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. 绑定初始数据

    有时,你的类不仅需要注入类,还需要注入一些原始数据,如一个整数。此时,你可以容易地通过情景绑定注入需要的任何值:

    $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'));
    });
    原文作者:TylerZou
    原文地址: https://segmentfault.com/a/1190000008844504
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞