xiaohuilam/laravel

10. 容器的 singleton 和 bind 的实现

Opened this issue · 2 comments

容器的 singletonbind 方法在整个 Laravel 框架或扩展中是调用的比较频繁的底层方法,掌握其原理能帮助我们加深 laravel 容器的理解。

初识

singleton() 代码

单身狗模式

/**
* Register a shared binding in the container.
*
* @param string $abstract
* @param \Closure|string|null $concrete
* @return void
*/
public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}

bind() 代码

分裂模式

/**
* Register a binding with the container.
*
* @param string $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);
}
}

初略看二者差异可能看不大出来。
我们从他们暴露的接口入手,先用,再啪(扒)源码。

调试

祭出大杀器:php artisan tinker

bind

>>> app()->bind('test', function(){dump(1); return new \StdClass();});
1
=> null
>>>
>>> app()->make('test')
1
=> {#2895}
>>>
>>> app()->make('test')
1
=> {#2896}

可以看出,调用 bind 时,每次取出被绑定的对象时,都会重新去构建一次。

如果用singleton 呢。

singleton

>>> app()->singleton('test', function(){dump(1); return new \StdClass();});
1
=> null
>>> app()->make('test')
=> {#2915}
>>> app()->make('test')
=> {#2915}
>>>

分析

bind 的实质,容器中捆绑

我们回头再看下 bind() 方法

/**
* Register a binding with the container.
*
* @param string $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);
}
}

逻辑步骤为:

  • 参数依次为 $abstract$concrete
  • dropStaleInstances 移除之前 bind 过的 $abstract 的数据(旧的 $concret
  • 如果没有传 $concrete,则将 $concrete 设置成 $abstract
  • 如果为闭包 如果不为闭包(感谢 @HubQin 的指正),则通过 Container::getClosure() 方法将闭包改成一个接受 $container$parameters 的包裹后的闭包。此闭包实质就是在需要运行的时候,运行闭包。
  • Container::$bindings 数组 $abstract 为 key 的值设置为 compact('concrete', 'shared'),即 ["concrete" => $concrete, "shared" => $shared]
  • 如果 bind 的对象已经 resolved 过了,通过 Container::rebound() 触发 reboundCallbacks 中的回调。

到目前为止,暂时还没看出 $sharesingletonbind 的影响。只知道 bind 在执行过程中,将 $shared$concrete 一起存到了 Container::$bindings 中。

用句四川话,莫慌,我们马上就要揭开关键的面纱了。。。

揭开 Container::make() 神秘的面纱

/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @return mixed
*/
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}

其实 make 是直接透传给了 resolve

/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @return mixed
*/
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
// If an instance of the type is currently being managed as a singleton we'll
// just return an existing instance instead of instantiating new instances
// so the developer can keep using the same objects instance every time.
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
// We're ready to instantiate an instance of the concrete type registered for
// the binding. This will instantiate the types, as well as resolve any of
// its "nested" dependencies recursively until all have gotten resolved.
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
// If we defined any extenders for this type, we'll need to spin through them
// and apply them to the object being built. This allows for the extension
// of services, such as changing configuration or decorating the object.
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// If the requested type is registered as a singleton we'll want to cache off
// the instances in "memory" so we can return it later without creating an
// entirely new instance of an object on each subsequent request for it.
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
// Before returning, we will also set the resolved flag to "true" and pop off
// the parameter overrides for this build. After those two things are done
// we will be ready to return back the fully constructed class instance.
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}

第一步骤的

$abstract = $this->getAlias($abstract);

是做了森么呢?
还原用 Container::alias() 方法设置过别名的原始 $abstract

下一步,

$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);

Container::getContextualConcrete() 其实是取这里设置进取的 上下文绑定 的数据

/**
* Define the implementation for the contextual binding.
*
* @param \Closure|string $implementation
* @return void
*/
public function give($implementation)
{
$this->container->addContextualBinding(
$this->concrete, $this->needs, $implementation
);
}

ContextualBindingBuilder::give() 暂时就不在这里暂开研究了。咱们继续往下看

$needsContextualBuild 值为 make() 方法是否传了第二个参数,也就是 $parametersneeds contextual build 意思是

构建时,是否具有上下文关联性

if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}

判断 Container::$instances 数组中是否能通过 $abstract 找到对象。如果找得到且不具有上下文关联性,就用之前创建好的对象。

$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);

getConcrete() 代码为

* @param string $abstract
* @return mixed $concrete
*/
protected function getConcrete($abstract)
{
if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
return $concrete;
}
// If we don't have a registered resolver or concrete for the type, we'll just
// assume each type is a concrete name and will attempt to resolve it as is
// since the container should be able to resolve concretes automatically.
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
return $abstract;
}

相信你能看出来 return $this->bindings[$abstract]['concrete'] ,就是根据 $abstract 把前面 bindsingleton 的数据返回。

这里的 isBuildable 是判断 $abstract 是否与 $concrete 一致或 $concrete 为闭包

如果不是,那么可能是还有递归调用,需要再走一次 make

if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}

645 行调用的 isBuildable()

protected function isBuildable($concrete, $abstract)
{
return $concrete === $abstract || $concrete instanceof Closure;
}

接着调用了 Container::build() 方法

public function build($concrete)
{
// If the concrete type is actually a Closure, we will just execute it and
// hand back the results of the functions, which allows functions to be
// used as resolvers for more fine-tuned resolution of these objects.
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
$reflector = new ReflectionClass($concrete);
// If the type is not instantiable, the developer is attempting to resolve
// an abstract type such as an Interface of Abstract Class and there is
// no binding registered for the abstractions so we need to bail out.
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
// If there are no constructors, that means there are no dependencies then
// we can just resolve the instances of the objects right away, without
// resolving any other types or dependencies out of these containers.
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
$dependencies = $constructor->getParameters();
// Once we have all the constructor's parameters we can create each of the
// dependency instances and then use the reflection instances to make a
// new instance of this class, injecting the created dependencies in.
$instances = $this->resolveDependencies(
$dependencies
);
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
}

其中的

$reflector = new ReflectionClass($concrete);
$dependencies = $constructor->getParameters();
// Once we have all the constructor's parameters we can create each of the
// dependency instances and then use the reflection instances to make a
// new instance of this class, injecting the created dependencies in.
$instances = $this->resolveDependencies(
$dependencies
);
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
是通过反射解析待构建类所需参数中其他对象参数,从容器中取出填入。

再往下面就是执行 Container::extend 设置的扩充流程了。

foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}

再后面,才是对我们 singleton()bind() 产生深远影响的代码:

if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}

我们在前面知道,如果为 singleton() 执行到 resolve() 的第二个参数 $sharedtrue, bindfalse
前面 $needsContextualBuild 判断的位置,判断了 Container::$instance 中是否有 $abstract, 所以在这里如果 resolve 没有将 $object 绑入 Container::$instance,那么下次 Container::make() 依旧会在去 build() 一次,这就产生了 singleton()bind()的特性。

再接着,就是触发事件了。

$this->fireResolvingCallbacks($abstract, $object);

最后是一些收尾工作

$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;

此章节为什么叫做单身狗呢?因为 Container::make() 严格遵守 $shared,如果声明了 true,他保证不搞一个分身(所以是单身)给你。

bind的分析,'如果为闭包' 应该是 '如果不是闭包' 吧?因为程序是 if (! $concrete instanceof Closure)

bind的分析,'如果为闭包' 应该是 '如果不是闭包' 吧?因为程序是 if (! $concrete instanceof Closure)

是的,打错了