laravel5.5框架解析[2]——容器与依赖注入

laravel5.5框架解析系列文章属于对laravel5.5框架源码分析,如有需要,建议按顺序阅读该系列文章, 不定期更新,欢迎关注

已经有很多文章写laravel的ioc了, 这篇文章浅谈一下其实现原理

laravel容器的作用:

  • 共享对象(准确来说是变量都可以共享, 对象共享用的多)
  • 依赖注入, 自动实例化.

PSR(php标准规范)定义的容器接口:

interface ContainerInterface
{
    public function get($id);

    public function has($id);
}

该接口只有2个方法: get 方法通过id取出值, has 方法判断id是否存在于容器, $idstring类型

这是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()干了啥

上面案例中首先创建了一个容器,在容器中绑定了ChargeAbleIPhone, 然后就通过容器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是递归的,先进后出. 栈也是递归当中常用的数据结构

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容