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...)
- 绑定初始数据
有时,你的类不仅需要注入类,还需要注入一些原始数据,如一个整数。此时,你可以容易地通过情景绑定注入需要的任何值:
$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'));
});