laravel源码阅读(绑定与解析服务)

1. 环境配置

  1. laravel5.1.46的源码
  2. PHPstorm开发工具
  3. 服务器集成环境(mamp、wamp)安装xdebug扩展

2. 关键点

  1. 使用PHPstorm+xdebug进行源码阅读时需点击(F7按钮)进入到下一个执行代码的方法内;
  2. laravel框架中绑定服务是将抽象的服务提供者和一个回调函数绑定在服务容器的bingings属性中;
  3. 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属性的值,如图:

绑定服务时记录的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方法,是通过反射类进行实例化的,这也就是传说中的控制反转的具体事例

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

推荐阅读更多精彩内容

  • 先说几句废话,调和气氛。事情的起由来自客户需求频繁变更,伟大的师傅决定横刀立马的改革使用新的框架(created ...
    wsdadan阅读 3,036评论 0 12
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • Laravel 学习交流 QQ 群:375462817 本文档前言Laravel 文档写的很好,只是新手看起来会有...
    Leonzai阅读 7,773评论 2 12
  • $bindings用于存储提供服务的回调函数$instances用于存储程序中共享的实例,也可以称为单例。 服务容...
    邱杉的博客阅读 971评论 0 50
  • 别人一句话,一个动作,一个眼神,都能让她想半天,甚至一天都闷闷不乐,太敏感了,内心世界太小了。要豁达,走出来,人生...
    葵影阅读 148评论 0 0