一、前言
在 laravel
框架中,如果问什么是设计最巧妙也是最需要掌握的内容,毫无疑问的就是服务容器。laravel
框架之所以能够具备低耦合、易扩展和可重用的优秀特性,也正是因为有了容器。如果将整个 laravel
框架比喻成一个人,那么服务容器就是相当于人的大脑,其重要性不言而喻。
下文就对 laravel
框架中的容器相关源代码进行解析,原文较长,但是我相信看完必然会有所收获。
二、源码解析
在 laravel
框架中,服务容器是通过 Illuminate\Container\Container
类实现的,俗话说,一图可抵千字,这里先给出容器类的工作示意图。
对于程序设计来说,源码是最好的老师。一切的概念通过描述或者加工后,都会存在意义上面的偏差,只有通过了解源码才能体会其中的含义。这里给出 laravel
框架中关于服务容器的部分源码,结合 laravel
框架容器类的示意图,进一步理解实现的方法和思想。具体代码如下:
2.1 服务绑定
/**
* The current globally available container (if any).
*
* @var static
*/
protected static $instance;
/**
* An array of the types that have been resolved.
*
* @var bool[]
*/
protected $resolved = [];
/**
* The container's bindings.
*
* @var array[]
*/
protected $bindings = [];
服务容器类中定义了用于管理服务的属性,分别是 $bindings
和 $instances
, 其中 $bindings
用于存储提供服务的回调函数, $instances
用于存储程序中所有共享的实例,也称之为单例,而 $resolved
存储已经解析过得实例。
/**
* 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);
}
上述代码就是注册一个单例到容器中
/**
* 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)
{
$this->dropStaleInstances($abstract);
if (is_null($concrete)) {
$concrete = $abstract;
}
if (! $concrete instanceof Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}
$this->bindings[$abstract] = compact('concrete', 'shared');
if ($this->resolved($abstract)) {
$this->rebound($abstract);
}
}
/**
* Get the Closure to be used when building a type.
*
* @param string $abstract
* @param string $concrete
* @return \Closure
*/
protected function getClosure($abstract, $concrete)
{
return function ($container, $parameters = []) use ($abstract, $concrete) {
if ($abstract == $concrete) {
return $container->build($concrete);
}
return $container->resolve(
$concrete, $parameters, $raiseEvents = false
);
};
}
这几个函数实现了 laravel
框架中服务容器的服务绑定功能,主要由bind()函数实现的。singleton()
函数实现的是单例绑定,即程序中如果没有服务名称对应的实例对象,则通过服务容器实例化后并进行记录,如果在后续程序中还需要同名的服务时则返回先前创建的服务实例对象。该函数相当于bind()函数的一个特例,即参数 $share
值为 true
的情况。对于 bind()
函数实现的服务绑定功能,在忽略 $shared
参数的情况下,即不讨论单例还是普通的服务,可以分为两种情况,如果参数 $concrete
为一个回调函数,则直接将回调函数与服务名称 $abstract
进行绑定;如果参数 $abstract
是一个名称,则首先需要通过 getClosure()
函数创建服务回调函数,然后将该回调函数与服务名称进行绑定,总之需要实现可以生成相应的服务实例对象的回调函数和服务名称进行绑定。
2.2 服务查找
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @param bool $raiseEvents
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
if ($raiseEvents) {
$this->fireResolvingCallbacks($abstract, $object);
}
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
/**
* Get the alias for an abstract if available.
*
* @param string $abstract
* @return string
*/
public function getAlias($abstract)
{
if (! isset($this->aliases[$abstract])) {
return $abstract;
}
return $this->getAlias($this->aliases[$abstract]);
}
/**
* Determine if the given concrete is buildable.
*
* @param mixed $concrete
* @param string $abstract
* @return bool
*/
protected function isBuildable($concrete, $abstract)
{
return $concrete === $abstract || $concrete instanceof Closure;
}
/**
* Get the concrete type for a given abstract.
*
* @param string $abstract
* @return mixed $concrete
*/
protected function getConcrete($abstract)
{
if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
return $concrete;
}
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
return $abstract;
}
服务解析的过程略微复杂一点,可以将其分为两个步骤来完成,一个是完成对应服务的查找,一个是完成对应服务的实现,一般是指完成实例化对象的创建,这两个步骤分别由 make()
和 bind()
函数完成。
首先这里先介绍服务查找的过程,即由 make()
函数实现的功能。该函数需要提供两个参数,分别是 $abstract
和 $parameters
, $abstract
可以看做是服务的名称,而 $parameters
是创建对象所需要的参数,即一个类实例化的依赖。对于服务的查找是根据服务名称 $abstract
来进行的,首先通过函数 getAlias()
来查找服务名称是否有别名,对于服务名的管理是通过服务容器类的 $aliases
数组属性实现的,而内容基本是基本上通过Illuminate\Foundation\Application
类中的函数 registerCoreContainerAliases
注册的,有一个简单的实例,抽象类的别名为 app
,如果查找到了别名,将查找到该别名对应的服务,如果该抽象类没有别名,则继续进行查找,然后在服务容器的共享实例数组(
i
n
s
t
a
n
c
e
s
属
性
)
中
查
找
服
务
名
称
的
实
例
,
如
果
查
找
到
则
说
明
该
服
务
名
称
为
单
例
,
则
直
接
返
回
先
前
实
例
化
的
对
象
,
否
则
再
继
续
查
询
,
接
下
来
会
通
过
‘
g
e
t
C
o
n
c
r
e
t
e
(
)
‘
获
取
服
务
名
称
的
实
体
,
在
服
务
绑
定
时
,
一
个
服
务
名
称
一
般
绑
定
一
个
回
调
函
数
用
于
生
成
实
例
对
象
,
而
这
个
回
调
函
数
就
相
当
于
服
务
名
称
的
实
体
,
这
个
实
体
的
查
找
就
是
通
过
容
器
中
的
(
instances属性)中查找服务名称的实例,如果查找到则说明该服务名称为单例,则直接返回先前实例化的对象,否则再继续查询,接下来会通过 `getConcrete()` 获取服务名称的实体,在服务绑定时,一个服务名称一般绑定一个回调函数用于生成实例对象,而这个回调函数就相当于服务名称的实体,这个实体的查找就是通过容器中的(
instances属性)中查找服务名称的实例,如果查找到则说明该服务名称为单例,则直接返回先前实例化的对象,否则再继续查询,接下来会通过‘getConcrete()‘获取服务名称的实体,在服务绑定时,一个服务名称一般绑定一个回调函数用于生成实例对象,而这个回调函数就相当于服务名称的实体,这个实体的查找就是通过容器中的(bindings)数组属性实现的,如果查找到返回实体,否则修改服务名称的形式,继续下一次查找。然后会通过 isBuildable()
函数来判断服务实体能否创建实名化对象,如果可以再转入下一步骤,否则继续通过 make()
函数来查找,在完成使用对象的创建后,通过 isShared()
判断该服务是否为单例,如果是需要在共享对象数组($instances
)对象中记录。
2.3 服务解析,实例化对象返回
//根据一个给定的服务名称实例化一个具体类对象
public function build($concrete)
{
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
$reflector = new ReflectionClass($concrete);
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
$dependencies = $constructor->getParameters();
try {
$instances = $this->resolveDependencies($dependencies);
} catch (BindingResolutionException $e) {
array_pop($this->buildStack);
throw $e;
}
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($dependency->getClass())
? $this->resolvePrimitive($dependency)
: $this->resolveClass($dependency);
}
return $results;
}
//通过容器解决一个类的依赖
protected function resolveClass(ReflectionParameter $parameter)
{
try {
return $this->make($parameter->getClass()->name);
}
// 省略异常处理部分
}
在通过 make()
函数查找到服务实体后,会将其传递给 bind()
函数用于对象的创建,如果服务实体就是一个闭包函数,则直接通过该闭包函数完成实例化对象的创建,如果服务实体只是一个具体类的类名,则需要通过反射机制来完成实例化对象的创建,通过反射机制完成对象实例化的过程,首先是将要实例化类名称获取反射类(ReflectionClass
)实例,然后获取该类在实例化过程中的依赖,即构造函数需要的参数,在 build()
函数中通过 函数 resolveDependencies
来实现依赖的生成,如果在服务解析时提供了相应的参数,即通过 $parameter
参数提供,则直接使用提供参数,如果没有提供,则通过服务容器中的 resolveNonClass()
函数来获取默认参数,或者通过 resolveClass
函数来创建,而创建的方式也是通过服务容器,所以服务容器解决依赖注入的问题,就是通过这部分代码实现的,在解决了依赖的问题后,可以直接通过反射机制完成服务实例对象的创建。
三、总结
一个对象在 laravel
框架中,从无到有,可能会经历几个阶段,服务绑定,服务查找,服务解析,通过在 laravel
框架源码的基础上分析服务容器的实现过程,应该会对服务容器的概念、IOC模式及依赖注入等概念有了进一步的了解,其中容器可以简单的理解为一个巨大且复杂的工厂,专门生产你所需要的对象,容器可以说是一个框架的核心和灵魂,有必要了解和掌握。