欢迎关注个人博客: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); } }
从源码中咱们能够看出,服务器的绑定有以下几个步骤:函数
去除原有注册。去除当前绑定接口的原有实现单例对象,和原有的别名,为实现绑定新的实现作准备。ui
加装闭包。若是实现类不是闭包(绑定自身或者绑定接口),那么就建立闭包,以实现 lazy 加载。this
注册。将闭包函数和单例变量存入 bindings 数组中,以备解析时使用。code
回调。若是绑定的接口已经被解析过了,将会调用回调函数,对已经解析过的对象进行调整。
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 绑定仅仅是 bind 绑定的一个 shared 为真的形式:
public function singleton($abstract, $concrete = null) { $this->bind($abstract, $concrete, true); }
不对接口进行解析,直接给接口一个实例做为单例对象。从下面能够看出,主要的工做就是去除接口在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 绑定通常用于依赖注入,当咱们利用依赖注入来自动实例化对象时,服务容器实际上是利用反射机制来为构造函数实例化它的参数,这个过程当中,被实例化的对象就是下面的 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; }
标签绑定比较简单,绑定过程就是将标签和接口之间创建一个对应数组,在解析的过程当中,按照标签把全部接口都解析一遍便可。
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 扩展分为两种,一种是针对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.