1. 环境配置
- laravel5.1.46的源码
- PHPstorm开发工具
- 服务器集成环境(mamp、wamp)安装xdebug扩展
2. 关键点
- 使用PHPstorm+xdebug进行源码阅读时需点击(F7按钮)进入到下一个执行代码的方法内;
- laravel框架中绑定服务是将抽象的服务提供者和一个回调函数绑定在服务容器的bingings属性中;
- laravel框架解析服务是将bindings属性中绑定的服务提供者的具体服务解析出来并实例化。
下面我将一一解析上面的关键点。
3. 绑定服务
1. 绑定服务
laravel框架中的绑定服务是使用bind方法,将一个抽象类(接口)和一个具体类绑定绑定在服务容器内,并记录在服务容器的bindings属性中,代码如下:
文件Illuminate\Container\Container.php
public function bind($abstract, $concrete = null, $shared = false)
{
// 判断抽象类是否是数组
if (is_array($abstract)) {
list($abstract, $alias) = $this->extractAlias($abstract);
$this->alias($abstract, $alias);
}
// 删除旧的实例和别名
$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);
}
}
当然我只是写出了这个方法的执行代码,由于篇幅有限,并未将此方法中调用的方法的代码列出来,请读者自行查看源码。
从上面的代码中可以看出绑定服务就是将一个抽象类和一个具体类记录在服务容器中的bindings属性中,其中抽象类作为键名,键值为一个数组,数组中的键值分别为concrete和shared,键值为$concrete和$shared,当然这样说是很笼统的,不如直接展示那么明显,下面展示代码执行时用xdebug查看bindings属性的值,如图:
2. 解析服务
laravel框架解析服务使用的是:make方法,通过传入一个抽象类(接口),获取具体的实例,在解析时通过shared的属性判断是否将获得的实例共享到服务容器的单例属性instances数组中,并已经解析的抽象类记录在resolved属性数组中,先看具体实现的代码:
文件Illuminate\Container\Container.php
public function make($abstract, $parameters = [])
{
// 获取别名
$abstract = $this->getAlias($abstract);
// 检测抽象类的实例是否在单例数组中
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
// 根据抽象类名获取绑定的具体类
$concrete = $this->getConcrete($abstract);
// 实例化具体类
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete, $parameters);
} else {
$object = $this->make($concrete, $parameters);
}
// 执行抽象类的扩展
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// 判断抽象类是否可共享
if ($this->isShared($abstract)) {
$this->instances[$abstract] = $object;
}
// 解析抽象类的所有回调
$this->fireResolvingCallbacks($abstract, $object);
// 将解析的抽象类记录在已解析数组中
$this->resolved[$abstract] = true;
// 返回实例对象
return $object;
}
理论需要经过实践验证,下面我将一个比较复杂的服务绑定过程和解析过程,(Illuminate\Contracts\Http\Kernel和App\Http\Kernel的绑定与解析)
3. 具体事例
还是按照上面的代码执行顺序,先看绑定服务部分,接着再说解析服务。
3.1)绑定服务
首先是在app.php文件中绑定服务的,代码如下:
文件bootstrap\app.php
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
# 这时有人就会说,这儿没有绑定动作,接着往下看
文件Illuminate\Container\Container.php
public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}
# bind方法就是上面代码中绑定服务的方法
从上面的代码可以看出,使用Singleton绑定服务时,在bindings数组中的Illuminate\Contracts\Http\Kernel的shared值是true,也就是说最后会存入到共享单例数组中的。
下面说说解析服务,这部分比较复杂
3.2)解析服务
文件public\index.php
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
文件Illuminate\Foundation\Application.php
public function make($abstract, array $parameters = [])
{
// 获取别名
$abstract = $this->getAlias($abstract);
// 判断是否在延迟加载的服务提供者数组中
if (isset($this->deferredServices[$abstract])) {
$this->loadDeferredProvider($abstract);
}
// 调用父类的make方法
return parent::make($abstract, $parameters);
}
解析服务是通过make方法解析的,下面看看具体的操作,首先,make中传入的是一个接口,运行到Application文件中的make方法获取这个接口的别名,判断是否在延迟加载的服务提供者数组中,最后调用了父类的make方法,也就是Container文件中的make方法;
父类的make方法中也是先获取别名,判断是否已经实例化过,获取与Illuminate\Contracts\Http\Kernel绑定的具体类,前面已经知道,与它绑定的是App\Http\Kernel这个类,下面就是判断是否可实例化,可实例化时调用build方法,build方法的代码如下:
文件Illuminate\Container\Container.php
public function build($concrete, array $parameters = [])
{
// 判断具体类是否是回调函数的实例
if ($concrete instanceof Closure) {
return $concrete($this, $parameters);
}
// 实例化反射类
$reflector = new ReflectionClass($concrete);
// 判断具体类是否可实例化
if (! $reflector->isInstantiable()) {
$message = "Target [$concrete] is not instantiable.";
throw new BindingResolutionContractException($message);
}
// 将具体类记录在实例化栈中
$this->buildStack[] = $concrete;
// 通过反射类获取具体类的构造函数
$constructor = $reflector->getConstructor();
// 判断构造函数是否为空
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
// 获取构造函数的参数
$dependencies = $constructor->getParameters();
// 通过参数名重新写入参数
$parameters = $this->keyParametersByArgument(
$dependencies, $parameters
);
// 获取依赖的参数
$instances = $this->getDependencies(
$dependencies, $parameters
);
// 将具体类推出栈
array_pop($this->buildStack);
// 通过反射类的参数实例化具体类
return $reflector->newInstanceArgs($instances);
}
现在我们知道需要调用build方法,传入的是回调函数,也就是App\Http\Kernel类封装的一个回调函数,在执行上面build方法的代码时,会执行$concrete($this, $parameters);,而这个是在绑定具体类时获得的回调函数,代码如下:
文件Illuminate\Container\Container.php
protected function getClosure($abstract, $concrete)
{
return function ($c, $parameters = []) use ($abstract, $concrete) {
$method = ($abstract == $concrete) ? 'build' : 'make';
return $c->$method($concrete, $parameters);
};
}
根据之前的代码知道$abstract的值是:Illuminate\Contracts\Http\Kernel,而$concrete的值是:App\Http\Kernel,所以调用make方法,而现在调用make方法时传入的是App\Http\Kernel,接着运行Illuminate\Foundation\Application.php文件中的make方法,获取App\Http\Kernel的别名,此类没有别名,调用父类的make方法,判断是否在单例数组中,接着是获取与此类绑定的具体类,没有就返回自己本身,下面就是调用build方法实例化此类,判断此类不是回调函数的实例,实例化反射类,并传入此类类名,判断此类是否可实例化,获得构造函数,并获取构造函数的参数,$app和$router,根据反射类的参数解决所有的依赖(获取依赖类的实例),代码如下:
文件Illuminate\Container\Container.php
public function getDependencies(array $parameters, array $primitives = [])
{
$dependencies = [];
foreach ($parameters as $parameter) {
$dependency = $parameter->getClass();
if (array_key_exists($parameter->name, $primitives)) {
$dependencies[] = $primitives[$parameter->name];
} elseif (is_null($dependency)) {
$dependencies[] = $this->resolveNonClass($parameter);
} else {
$dependencies[] = $this->resolveClass($parameter);
}
}
return (array) $dependencies;
}
public function resolveClass(ReflectionParameter $parameter)
{
try {
return $this->make($parameter->getClass()->name);
} catch (BindingResolutionContractException $e) {
if ($parameter->isOptional()) {
return $parameter->getDefaultValue();
}
throw $e;
}
}
上面的代码是为了解决一个类的依赖问题,而App\Http\Kernel的依赖是Illuminate\Contracts\Foundation\Application和Illuminate\Routing\Router,所以通过resolveClass方法传入$app来获取Illuminate\Contracts\Foundation\Application的实例,传入$router获取Illuminate\Routing\Router的实例。
这里的实例化也是通过的是make方法进行实例化,$app->getClass()->name之后获得的值是:Illuminate\Contracts\Foundation\Application,而这个接口是有别名的,别名是app,而app之前已经实例化了,存储在$instances数组中,获取了app的实例存入到依赖数组中,同理获得router的实例也存入到依赖数组中,接着返回到Container中make方法里的$instances变量中,在build方法最后一行代码中实例化了App\Http\Kernel类,那就返回到Container的make方法,并将App\Http\Kernel记录在已解析的数组中,返回App\Http\Kernel的实例对象,还是返回到make方法(此处是执行解析Illuminate\Contracts\Http\Kernel的make方法中),并将Illuminate\Contracts\Http\Kernel也记录在已解析的数组中。
4. 总结
上面描述了服务的绑定与解析的核心过程,在绑定和解析的过程中牵扯到了下面几个方法:
1). 绑定服务的方法bind,还有Singleton方法,使用Singleton绑定的服务最后解析之后是共享在单例数组中的;
2). 解析服务的方法make,当然主要的解析过程是在父类(Container类)中make方法,具体实例化是在build方法,是通过反射类进行实例化的,这也就是传说中的控制反转的具体事例。