thinkphp中容器的实现

上一章节我们讲了tp使用的自动加载的机制,这一章接下来就到了tp的核心处理部分了,先看代码:

// 执行HTTP应用并响应
$http = (new App())->http;

App基础类

上面代码里有行注释:“执行HTTP应用并响应”,这句话说的已经比较清楚了,我们具体分析代码,new App()是实例化一个叫做App的类,这个对象是上面自动加载进来的,我们进入这个类,查看这个类的源码:

/**
 * App 基础类
 *************(注释部分省略,详细请看tp源码)
 */
class App extends Container

容器管理类

可以看到这个类是App基础类,也就是thinkphp的基础类,它继承了Container类,所以我我们继续进入Container类中去看它的源码:

/**
 * 容器管理类 支持PSR-11
 */
class Container implements ContainerInterface, ArrayAccess, IteratorAggregate, Countable

我们可以看到,这个容器类又继承了ContainerInterface, ArrayAccess, IteratorAggregate, Countable这么多的接口,我们一个一个分析。

生成容器的接口类

ContainerInterface 接口类

首先我们可以看ContainerInterface接口类,可以看到这个接口类是用composer从PHP的packageist库里加载进来的包,是PSR-11规范的容器接口(参考这两篇:PSR-11 容器接口PSR-11 容器接口 - 说明文档)。可以把这个接口类看做一个容器的规范,在PHP框架中具体实现的容器功能是根据这个规范来走的。

ArrayAccess 接口类

这个接口是PHP内置的接口类,提供像访问数组一样访问对象的能力的接口,普通的类继承并实现ArrayAccess接口之后,就可以在实例化对象后可以用操作数组的方式来操作对象内的属性。

IteratorAggregate 接口类

这个接口也是PHP内置的接口类,首先看PHP手册中的介绍:“创建外部迭代器的接口”。

Countable 接口类

这个接口类同样也是PHP内置的接口类,首先看PHP手册的介绍:“类实现 Countable 可被用于 count() 函数”,这个接口的主要作用是可以用来统计对象内部某些元素的个数。

接下来就是见证奇迹的时刻!我们看一下thinkphp如何实现的容器功能:

/**
 * 获取容器中的对象实例
 * @access public
 * @param string $abstract 类名或者标识
 * @return object
 */
public function get($abstract)
{
  if ($this->has($abstract)) {
    return $this->make($abstract);
  }

  throw new ClassNotFoundException('class not exists: ' . $abstract, $abstract);
}
/**
 * 判断容器中是否存在类及标识
 * @access public
 * @param string $name 类名或者标识
 * @return bool
 */
public function has($name): bool
{
  return $this->bound($name);
}

一个小问题:get()方法是怎么被调用的呢?
其实是用到了魔术方法__get(),在魔术方法__get()中调用了get()方法,不了解魔术方法的童鞋请自行移步PHP手册。

上面这两段两个方法就是对接口方法的具体实现,可以看到,获取方法get()是用来获取容器中类的,在其中调用了第二个方法has(),这个方法是用来判断容器中是否存在的,我们可以看到,在has()方法中调用了bound()方法,我们来看一下bound()方法的具体代码:

/**
 * 判断容器中是否存在类及标识
 * @access public
 * @param string $abstract 类名或者标识
 * @return bool
 */
public function bound(string $abstract): bool
{
  return isset($this->bind[$abstract]) || isset($this->instances[$abstract]);
}

由上面的代码可知,这个方法是判断$this->bind$this->instances这两个属性中有没有这个类,看到这里你心里可能会有点疑问,这两个属性中的值是哪里来的呢,看当前类中的这两个方法是空值,那么我们就可以想到,应该是继承这个Container类的子类给这两个属性赋值了,所以我们找到本文最开始看的那个App基础类,在其中找到了$bind属性值的重新初始化,上面有一行注释:容器绑定标识,这个属性的作用是用来用一个个简短的标识来标识对应要绑定到容器的类的,就类似于一个标识指向的作用,然后我们看一下$instances,发现这个属性没有在App基础类中重新初始化,但是在当前Container类中找到了给它赋值的方法:

/**
 * 绑定一个类实例到容器
 * @access public
 * @param string $abstract 类名或者标识
 * @param object $instance 类的实例
 * @return $this
 */
public function instance(string $abstract, $instance)
{
  $abstract = $this->getAlias($abstract);

  $this->instances[$abstract] = $instance;

  return $this;
}

可以看到,这个方法是用来绑定类实例到容器的,也就是说在这一步才真正把一个类绑定到容器里面,也就是绑定到$instances里面,这个$instances属性的作用就是存储被容器绑定的类,在上面的方法中$instances被赋值了,赋值的内容是用getAlias()方法获取来的,所以接下来我们看一下这个getAlias()方法:

/**
 * 根据别名获取真实类名
 * @param  string $abstract
 * @return string
 */
public function getAlias(string $abstract): string
{
  if (isset($this->bind[$abstract])) {
    $bind = $this->bind[$abstract];

    if (is_string($bind)) {
      return $this->getAlias($bind);
    }
  }

  return $abstract;
}

我们看到这里就明白了,instance()方法,先从$bind这个属性中取出来具体要绑定的类,然后把它存储到$instances属性之中。

所以回到get()方法中,这个方法中的第一行if ($this->has($abstract))已经解释明白了,就是当需要一个类的时候,比如文章需要的http类,就用has()方法先找,假如说找不到,就抛出错误throw new ClassNotFoundException('class not exists: ' . $abstract, $abstract);,假如找到了,就执行return $this->make($abstract);,抛出错误的逻辑我们暂时先不分析了,以后如果有空的话会单独写一章来分析其具体实现,我们接下来分析重头戏:return $this->make($abstract);

我们先看一下make()方法的具体实现:

/**
 * 创建类的实例 已经存在则直接获取
 * @access public
 * @param string $abstract    类名或者标识
 * @param array  $vars        变量
 * @param bool   $newInstance 是否每次创建新的实例
 * @return mixed
 */
public function make(string $abstract, array $vars = [], bool $newInstance = false)
{
  $abstract = $this->getAlias($abstract);

  if (isset($this->instances[$abstract]) && !$newInstance) {
    return $this->instances[$abstract];
  }

  if (isset($this->bind[$abstract]) && $this->bind[$abstract] instanceof Closure) {
    $object = $this->invokeFunction($this->bind[$abstract], $vars);
  } else {
    $object = $this->invokeClass($abstract, $vars);
  }

  if (!$newInstance) {
    $this->instances[$abstract] = $object;
  }

  return $object;
}

前三行我们前面已经分析过了,如果已存在就直接获取,我们具体看instances属性中没有的情况,看代码的逻辑,大体意思是先找到这个类,然后把它存在instances属性中,然后把类return回去,找这个类的时候分为两种情况:

  1. 如果bind属性中有,并且是个匿名函数,就执行函数或者闭包方法:$this->invokeFunction($this->bind[$abstract], $vars)
  2. 如果不是匿名函数,或者不在bind属性中,那就调用反射执行类的实例化:$this->invokeClass($abstract, $vars)

我们先来看第一种情况,老规矩,先上代码:

/**
 * 执行函数或者闭包方法 支持参数调用
 * @access public
 * @param string|Closure $function 函数或者闭包
 * @param array          $vars     参数
 * @return mixed
 */
public function invokeFunction($function, array $vars = [])
{
  try {
    $reflect = new ReflectionFunction($function);
  } catch (ReflectionException $e) {
    throw new FuncNotFoundException("function not exists: {$function}()", $function, $e);
  }

  $args = $this->bindParams($reflect, $vars);

  return $function(...$args);
}

先尝试获取这个函数的反射类,如果获取不到,就抛出错误,如果获取到了,那就调用bindParams()方法来处理反射类并生成函数的参数的数组,用...这种语法把这个数组变成函数参数,下面我们来简单分析一下这个bindParams()方法:

/**
 * 绑定参数
 * @access protected
 * @param ReflectionFunctionAbstract $reflect 反射类
 * @param array                      $vars    参数
 * @return array
 */
protected function bindParams(ReflectionFunctionAbstract $reflect, array $vars = []): array
{
  if ($reflect->getNumberOfParameters() == 0) {
    return [];
  }

  // 判断数组类型 数字数组时按顺序绑定参数
  reset($vars);
  $type   = key($vars) === 0 ? 1 : 0;
  $params = $reflect->getParameters();
  $args   = [];

  foreach ($params as $param) {
    $name      = $param->getName();
    $lowerName = Str::snake($name);
    $class     = $param->getClass();

    if ($class) {
      $args[] = $this->getObjectParam($class->getName(), $vars);
    } elseif (1 == $type && !empty($vars)) {
      $args[] = array_shift($vars);
    } elseif (0 == $type && isset($vars[$name])) {
      $args[] = $vars[$name];
    } elseif (0 == $type && isset($vars[$lowerName])) {
      $args[] = $vars[$lowerName];
    } elseif ($param->isDefaultValueAvailable()) {
      $args[] = $param->getDefaultValue();
    } else {
      throw new InvalidArgumentException('method param miss:' . $name);
    }
  }

  return $args;
}

通读这个方法可以看出,这个方法是用反射类把参数名都取出来,经过把驼峰转为下划线等一系列操作之后,放到一个数组里,然后return回去。

我们再来看第二种情况,老规矩,上代码:

/**
 * 调用反射执行类的实例化 支持依赖注入
 * @access public
 * @param string $class 类名
 * @param array  $vars  参数
 * @return mixed
 */
public function invokeClass(string $class, array $vars = [])
{
  try {
    //这里尝试创建一个反射类
    $reflect = new ReflectionClass($class);
  } catch (ReflectionException $e) {
    //如果失败的话就抛出异常
    throw new ClassNotFoundException('class not exists: ' . $class, $class, $e);
  }
//如果这个类中存在__make,就用反射类获取__make这个方法,这里的原因解释一下:因为thinkphp底层的一些类是用__make()来代替构造方法,所以要判断是否存在__make()
  if ($reflect->hasMethod('__make')) {
    $method = $reflect->getMethod('__make');
    //如果这个方法的属性是public,并且是静态方法,就把它的参数取出来,存入$args数组中
    if ($method->isPublic() && $method->isStatic()) {
      $args = $this->bindParams($method, $vars);
      //执行这个方法并获取返回值
      return $method->invokeArgs(null, $args);
    }
  }
//如果这个类中不存在__make或者__make不是静态公共方法,那就继续往下走
  //在这里拿到构造方法
  $constructor = $reflect->getConstructor();
//获取构造方法的参数
  $args = $constructor ? $this->bindParams($constructor, $vars) : [];
//传入参数实例化class,获取这个类的新实例对象
  $object = $reflect->newInstanceArgs($args);
//调用invokeAfter(),执行回调(看该方法的具体代码可知,如果有的话会自动执行)
  $this->invokeAfter($class, $object);
//把生成的新实例return回去
  return $object;
}

我把具体的解析放在代码注释里了,这一步走完之后,make()的使命就结束了,我们成功的获取到了所需的对象,回到get()方法,我们把make()获取的对象return回去,这就完成了整个流程。

这就是thinkphp用反射实现容器功能的基本流程。

回到刚开始的$http = (new App())->http;

根据上面的分析,我们可以很轻松的知道,这句话其实就是通过容器获取了一个http的类的实例。

接下来就到了具体的处理请求的环节了,我们下一章再见!