laravel5.5框架解析系列文章属于对laravel5.5框架源码分析,如有需要,建议按顺序阅读该系列文章, 不定期更新,欢迎关注
已经有很多文章写laravel的ioc了, 这篇文章浅谈一下其实现原理
laravel容器的作用:
- 共享对象(准确来说是变量都可以共享, 对象共享用的多)
- 依赖注入, 自动实例化.
PSR(php标准规范)定义的容器接口:
interface ContainerInterface
{
public function get($id);
public function has($id);
}
该接口只有2个方法: get
方法通过id取出值, has
方法判断id是否存在于容器, $id
为 string
类型
这是laravel拓展的容器接口:
interface Container extends ContainerInterface
{
// 抽象类型(接口,抽象类)是否被绑定过
public function bound($abstract);
// 给抽象类型取一个别名, 用于依赖关系处理
public function alias($abstract, $alias);
// 给一组抽象类型打上一组标签.这一组抽象类里的每一个抽象类都会打上tags里的所有标签. 打上标签后,可以通过tag取出抽象类
public function tag($abstracts, $tags);
// 取出被打上tag标签的所有抽象类型
public function tagged($tag);
/**
* 绑定一个接口到实现(实现类的类名, 或用闭包返回一个实例)
*
* @param string|array $abstract 接口名称
* @param \Closure|string|null $concrete 实现类
* @param bool $shared 是否共享, 为true时每次会返回相同对象, 就是单例模式了
* @return void
*/
public function bind($abstract, $concrete = null, $shared = false);
// 没有被绑定过才绑定
public function bindIf($abstract, $concrete = null, $shared = false);
// 绑定单例, 即bind方法的share参数为ture
public function singleton($abstract, $concrete = null);
//绑定一个回调到抽象类, 当类被实例化时会调用这个回调并传入实例, 你可以在回调中动态的添加属性到实例, 即实现拓展了一个抽象类型
public function extend($abstract, Closure $closure);
// 也是绑定接口到实现,只是该实现是一个已经存在的实例
public function instance($abstract, $instance);
/**
* 设定条件, 因为不同的地方,你可能需要同一个接口的不同实现类, 通过when设定条件, 像这样使用
* $contianer->when(A::class)->needs(InterfaceB::class)->give(C::class);
* $contianer->when(D::class)->needs(InterfaceB::class)->give(E::class);
*
* @param string $concrete 条件类名
* @return \Illuminate\Contracts\Container\ContextualBindingBuilder
*/
public function when($concrete);
// 返回一个闭包, 调用这个闭包就能从容器得到$abstract的实现实例
public function factory($abstract);
// 从容器获取实现类实例, 可以传参给实现类构造函数, 因为有些参数容器无法处理, 比如某实现类构造方法需要一个int 参数, 你必须告诉容器int参数值具体是多少
public function make($abstract, array $parameters = []);
// 调用指定方法(像这样 ClassA::class . '@methodA'),或闭包 . 容器会make出类实例, 然后解析方法需要的依赖,注入调用方法,最后返回结果
public function call($callback, array $parameters = [], $defaultMethod = null);
// 判断抽象类是否已经resolved, 从一个抽象类型创建一个实现类实例的过程叫resolve
public function resolved($abstract);
// resolve 之前的回调, 回调接收2个参数: 这里的$abstract 和具体的实例
public function resolving($abstract, Closure $callback = null);
// resolve之后的回调
public function afterResolving($abstract, Closure $callback = null);
}
可以看到, laravel为容器增加了自动实例化的功能.以及调用类方法注入参数的功能
实现原理
其实要实现psr 的容器很简单, 你甚至可以稍微封装一下数组就能够实现. psr 容器实际上就是个key=>value的map数组.这里重点讲一下laravel如何实现依赖注入
使用案例
看如下代码
// 可充电设备
interface ChargedAble
{
public function getCharged(int $power);
}
// iPhone...
class IPhone implements ChargedAble
{
public function getCharged(int $power)
{
echo "the iPhone is being charged with $power volt power\n";
}
}
// 充电宝
class PowerBank
{
protected $volt = 5;
protected $device;
public function __construct(ChargedAble $device)
{
$this->device = $device;
}
public function charge()
{
$this->device->getCharged($this->volt);
}
}
$container = new Container();
$container->bind(ChargedAble::class, IPhone::class);
$powerBank = $container->make(PowerBank::class);
$powerBank->charge();
// $this is a "phpunit test case"
$this->expectOutputString("the iPhone is being charged with 5 volt power\n");
这是一个充电宝给手机充电的例子, 当然逻辑有点问题, 手机应该是在charge的时候传给充电宝, 而不是充电宝一出生就注定了一生所爱, but it's not the point. 能说明问题就行了
make()干了啥
上面案例中首先创建了一个容器,在容器中绑定了ChargeAble
和IPhone
, 然后就通过容器make了一台充电宝, 能够正常使用. 充电宝所得到的IPhone是怎么来的呢? Illuminate\Container
源码, make方法就是直接调用了这个resolve
protected function resolve($abstract, $parameters = [])
{
// 如果这个抽象类是一个别名(通过上面的alias方法定义的别名, 所谓定义别名就是用k->v数组存起来),
// 那么这里会被映射到被取别名的抽象类
$abstract = $this->getAlias($abstract);
// 这里判断是不是之前设定过条件绑定, 这个结果用来决定是返回缓存实例还是新间一个实例,
// 所以还`||`上了是否有参数, 有参数传进来当然不能返回之前的缓存了, 得用参数构造新的
$needsContextualBuild = ! empty($parameters) || ! is_null(
// 这里用到了一个biuld栈, 后面会有提及
$this->getContextualConcrete($abstract)
);
// 有缓存就直接返回缓存
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
// with数组是一个栈, 参数入栈是为了解决多层依赖的问题, 如a依赖b,b依赖c, 可能会递归调用make方法
$this->with[] = $parameters;
// 获取实现类名称. 其实绑定的时候把关系存数组了, 直接从数组里取,
// 如果没绑定, 那认为你想创建的就是$abstract, 直接返回
$concrete = $this->getConcrete($abstract);
// isBuildable 判断$concrete是不是可实例化
if ($this->isBuildable($concrete, $abstract)) {
创建实例, 具体在下边, 一会再看.
$object = $this->build($concrete);
} else {
// 不可实例化, 说明这个$concrete还是个抽象类型, 递归调用make
$object = $this->make($concrete);
}
// 调用上面说过的拓展回调
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// 把结果加到缓存里边
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
// 调用resolving回调(是不是发现有个resolved 回调没被调用, 其实在fireResolvingCallbacks方法里调用完resolving 就调用了resolved)
$this->fireResolvingCallbacks($abstract, $object);
// 标记一下这个类被resolve过了, 上面容器接口里的resolved方法就是靠这个标记来返回某个类是否被resolve过
$this->resolved[$abstract] = true;
// 参数出栈
array_pop($this->with);
// 返回结果
return $object;
}
// 创建一个类实例
public function build($concrete)
{
// 如果是个闭包, 那好办,直接通过调用闭包就完成了创建
if ($concrete instanceof Closure) {
// getLastParameterOverride, 这个方法会从上面resolve方法提到的参数栈里获取所需参数
return $concrete($this, $this->getLastParameterOverride());
}
// 反射, 不会的同学参考手册哦: http://php.net/manual/en/class.reflectionclass.php
$reflector = new ReflectionClass($concrete);
// 判断是否能够实例化
if (! $reflector->isInstantiable()) {
// 不能实例化的话, 这里其实是直接抛异常
return $this->notInstantiable($concrete);
}
// 实现类入 build 栈, 在处理条件绑定的时候会用到
$this->buildStack[] = $concrete;
// 构造方法的反射
$constructor = $reflector->getConstructor();
// 没有构造方法, 就不需要处理依赖了, 直接 new 即可
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
// 构造方法参数反射实例数组, 即需要被注入的依赖
$dependencies = $constructor->getParameters();
// 获取依赖, 具体步骤再下边
$instances = $this->resolveDependencies(
$dependencies
);
// 出 build 栈
array_pop($this->buildStack);
// 注入依赖, 创建实例, 并返回结果
return $reflector->newInstanceArgs($instances);
}
// 把依赖都resolve出来, $dependencies 是参数反射数组
protected function resolveDependencies(array $dependencies)
{
// 结果数组
$results = [];
// 遍历依赖数组, resolve出各个依赖参数
foreach ($dependencies as $dependency) {
// 在这个方法:resolve($abstract, $parameters = [])里
// 不是可以指定创建实例所需参数吗, 如果依赖已经被传过来了, 那就直接放到结果数组里
if ($this->hasParameterOverride($dependency)) {
// 这里获取传过来的依赖,就利用了上面的参数栈
$results[] = $this->getParameterOverride($dependency);
continue;
}
// 如果依赖是一个class, 那就resolve 这个 class,
// 也就是继续调用make把这个class实例取出来,
// 只是多了一步, 如果make报错了, 就看这个参数是不是有默认值, 有的话就返回默认值
// 如果依赖不是class, 而是基本类型呢?
// 那会尝试是不是有过条件绑定, 即$container->when($classname)->need($dep)->give($sth);
// 如果有绑定过$need为这个依赖参数的变量名, 那就会返回$sth,
// 如果没有的话,看该依赖参数是不是有默认值, 如果还没有, 那没办法,只能抛异常了
$results[] = is_null($dependency->getClass())
? $this->resolvePrimitive($dependency)
: $this->resolveClass($dependency);
}
// 返回结果数组
return $results;
}
就这样 $container->make(PowerBank::class)
就得到了充电宝实例. 整个流程应该算比较清晰的.
关于参数栈和build栈, 这2个栈是用来保存当前正在resolve的参数和类, 因为resolve是递归的,先进后出. 栈也是递归当中常用的数据结构