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);注意:如果是单例,则只在解析时会触发一次
额外补充[参考]()
-
绑定初始数据
有时,你的类不仅需要注入类,还需要注入一些原始数据,如一个整数。此时,你可以容易地通过情景绑定注入需要的任何值:
$this->app->when('AppHttpControllersUserController')
->needs('$variableName')->give($value); -
绑定接口至实现
服务容器有一个强大的功能,就是将一个指定接口的实现绑定到接口上。例如,如果我们有一个 EventPusher 接口和一个它的实现类 RedisEventPusher 。编写完接口的 RedisEventPusher 实现类后,我们就可以在服务容器中像下面例子一样注册它:
$this->app->bind(
'App\Contracts\EventPusher','App\Services\RedisEventPusher'
);
-
情境绑定
有时候,你可能有两个类使用到相同的接口,但你希望每个类都能注入不同的实现。例如,两个控制器可能需要依赖不同的 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');
});
-
标记
有时候,你可能需要解析某个「分类」下的所有绑定。例如,你正在构建一个报表的聚合器,它需要接受不同 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')); });