thinkphp 5.1框架解析(三):容器和依赖注入

在上一篇文章中我们讲到了 ThinkPHP 如何实现自动加载,如果想看的话可以看
ThinkPHP5.1 源码浅析(二)自动加载机制

在阅读本篇文章 之前,我希望你掌握了 IOC 、DI 、Facade的基本知识,如果不了解,请先查看着几篇文章。

深入理解控制反转(IoC)和依赖注入(DI)

那么步入正题。

服务调用

基于分析框架的 入口脚本文件index.php

// 加载基础文件
require __DIR__ . '/../thinkphp/base.php';

// 支持事先使用静态方法设置Request对象和Config对象

// 执行应用并响应
Container::get('app')->run()->send();

上面 base.php 中的作用是载入自动加载机制,和异常处理,以及开启日志功能。

// 执行应用并响应
Container::get('app')->run()->send();

在这里才是使用了 IOC 容器功能,获取 app 这个容器1

进入 Container 中之后我们先介绍他的类属性

protected static $instance; // 定义我们的容器类实例,采用单例模式,只实例化一次
protected $instances = [];    // 容器中的对象实例
protected $bind = [];        // 容器绑定标识
protected $name = [];        // 容器标识别名

$instances 是实现了 注册树模式2,存储值

array (
  'think\\App' => App实例,
  'think\\Env' => Env实例,
  'think\\Config' => Config实例,
   ...
)

bind 在初始化时,会载入一堆初始数据,记录一堆类别名和 类名的映射关系。

protected $bind = [
      'app' => 'think\\App',
      'build' => 'think\\Build',
      'cache' => 'think\\Cache',
      'config' => 'think\\Config',
      'cookie' => 'think\\Cookie',
        ...
    ]

namebind 属性记录的值都是很类似的,都是 类别名和 类名的映射关系。区别是,name 记录的是 已经实例化后的 映射关系。

进入get方法

public static function get($abstract, $vars = [], $newInstance = false)
{
    return static::getInstance()->make($abstract, $vars, $newInstance);
}

这一段代码没什么好讲的,就是先获取当前容器的实例(单例),并实例化。

进入 make 方法

public function make($abstract, $vars = [], $newInstance = false)
{
    if (true === $vars) {
        // 总是创建新的实例化对象
        $newInstance = true;
        $vars        = [];
    }
    // 如果已经存在并且实例化的类,就用别名拿到他的类
    $abstract = isset($this->name[$abstract]) ? $this->name[$abstract] : $abstract;
    // 如果已经实例化,并且不用每次创建新的实例的话,就直接返回注册树上的实例
    if (isset($this->instances[$abstract]) && !$newInstance) {
        return $this->instances[$abstract];
    }
    // 如果我们绑定过这个类,例如 'app' => 'think\\App',
    if (isset($this->bind[$abstract])) {
        $concrete = $this->bind[$abstract];
        // 因为ThinkPHP 实现可以绑定一个闭包或者匿名函数进入,这里是对闭包的处理
        if ($concrete instanceof Closure) {
            $object = $this->invokeFunction($concrete, $vars);
        } else {
            // 记录 映射关系,并按照 类名来实例化,如 think\\App
            $this->name[$abstract] = $concrete;
            return $this->make($concrete, $vars, $newInstance);
        }
    } else {
        // 按照类名调用该类
        $object = $this->invokeClass($abstract, $vars);
    }

    if (!$newInstance) {
        $this->instances[$abstract] = $object;
    }
    // 返回制作出来的该类
    return $object;
}

我们拆分一下,

if (true === $vars) {
        // 总是创建新的实例化对象
        $newInstance = true;
        $vars        = [];
}

这段代码是 让我们函数可以 使用 make($abstract, true)的方式调用此函数,使我们每次得到的都是新的实例。(我觉得这种方式不是很好,每个变量的造成含义不明确)

// 如果已经存在并且实例化的类,就用别名拿到他的类
    $abstract = isset($this->name[$abstract]) ? $this->name[$abstract] : $abstract;
    // 如果已经实例化,并且不用每次创建新的实例的话,就直接返回注册树上的实例
    if (isset($this->instances[$abstract]) && !$newInstance) {
        return $this->instances[$abstract];
    }

前面说过,name 中存放的是已经实例化的 别名=> 类名 的映射关系,我们在这里尝试取出 类名,如果该类实例化,就直接返回。

// 如果我们绑定过这个类,例如 'app' => 'think\\App',
    if (isset($this->bind[$abstract])) {
        $concrete = $this->bind[$abstract];
        // 因为ThinkPHP 实现可以绑定一个闭包或者匿名函数进入,这里是对闭包的处理
        if ($concrete instanceof Closure) {
            $object = $this->invokeFunction($concrete, $vars);
        } else {
            // 记录 映射关系,并按照 类名来实例化,如 think\\App
            $this->name[$abstract] = $concrete;
            return $this->make($concrete, $vars, $newInstance);
        }
    } else {
        // 按照类名调用该类
        $object = $this->invokeClass($abstract, $vars);
    }

这里是看我们需要容器加载的类是否以前绑定过别名(我们也可以直接 bind('classNickName') 来设置一个)

  1. 如果绑定过,那么就来实例化它。
  2. 如果没有,那么就认定他是一个类名,直接调用。3

门面模式

在上面的 IOC 容器中,我们需要 $ioc->get('test'); 才能拿到 test 类,才能使用我们的 $user->hello()方法进行打招呼,有了门面之后,我们可以直接 用 Test::hello() 进行静态调用,下面我们就来介绍一下这个

在我们编写代码时经常会用到 facade包下的类来接口的静态调用,我们在这里举一下官网的例子

假如我们定义了一个app\common\Test类,里面有一个hello动态方法。

<?php
namespace app\common;

class Test
{
    public function hello($name)
    {
        return 'hello,' . $name;
    }
}

调用hello方法的代码应该类似于:

$test = new \app\common\Test;
echo $test->hello('thinkphp'); // 输出 hello,thinkphp

接下来,我们给这个类定义一个静态代理类app\facade\Test(这个类名不一定要和Test类一致,但通常为了便于管理,建议保持名称统一)。

<?php
namespace app\facade;

use think\Facade;

class Test extends Facade
{
    protected static function getFacadeClass()
    {
        return 'app\common\Test';
    }
}

只要这个类库继承think\Facade,就可以使用静态方式调用动态类app\common\Test的动态方法,例如上面的代码就可以改成:

// 无需进行实例化 直接以静态方法方式调用hello
echo \app\facade\Test::hello('thinkphp');

结果也会输出 hello,thinkphp

说的直白一点,Facade功能可以让类无需实例化而直接进行静态方式调用。

Facade工作原理

  1. Facede 核心实现原理就是在 Facade 提前注入IoC容器。
  2. 定义一个服务提供者的外观类,在该类定义一个类的变量,跟ioc容器绑定的key一样,
  3. 通过静态魔术方法__callStatic可以得到当前想要调用的 hello 方法
  4. 使用static::$ioc->make('Test');

为什么要使用 Facade

使用Facades其实最主要的就是它提供了简单,易记的语法,从而无需手动注入或配置长长的类名。此外,由于他们对 PHP 静态方法的独特调用,使得测试起来非常容易。


  1. 在这里系统找不到 Container 类的位置,所以会执行自动加载机制去寻找 Container 的位置,并加载它 ↩
  2. 把一堆实例挂在树上,需要的时候在拿来用。 ↩
  3. 直接调用是使用了反射后的结果,关于反射的知识点在自行查看 ↩

相关推荐