1. 概述

在开始使用TP5的时候,发现,为啥子很多TP框架自带的类,在其他目录都有一个相同的类文件。比如: Cookie , Log , Request , Cache , … ,等。
image.png
image.png
因此,看看这是什么作用。以 Cache::set() 为例。

1.1 应用

  1. Cache 的应用:

平常运用一样,先 new 一个对象,然后通过 -> 调用方法:

  1. <?
  2. use \think\Cache;
  3. $c = new Cache();
  4. $c->set($name, $value, $expire);
  1. facade\Cache 的应用:

静态的方式访问:

<?
use \think\facade\Cache;

Cache::set($name, $value, $expire);

2. 分析

2.1 查看第一层源码

  1. Cache源码就是具体实现的源码,调试代码也是走到该类。
  2. 查看facade\Cache 源码:

    <?php
    namespace think\facade;
    use think\Facade;
    /**
    * @see \think\Cache
    * @mixin \think\Cache
    * @method \think\cache\Driver connect(array $options = [], mixed $name = false) static 连接缓存
    * @method \think\cache\Driver init(array $options = []) static 初始化缓存
    * @method \think\cache\Driver store(string $name = '') static 切换缓存类型
    * @method bool has(string $name) static 判断缓存是否存在
    * @method mixed get(string $name, mixed $default = false) static 读取缓存
    * @method mixed pull(string $name) static 读取缓存并删除
    * @method mixed set(string $name, mixed $value, int $expire = null) static 设置缓存
    * @method mixed remember(string $name, mixed $value, int $expire = null) static 如果不存在则写入缓存
    * @method mixed inc(string $name, int $step = 1) static 自增缓存(针对数值缓存)
    * @method mixed dec(string $name, int $step = 1) static 自减缓存(针对数值缓存)
    * @method bool rm(string $name) static 删除缓存
    * @method bool clear(string $tag = null) static 清除缓存
    * @method mixed tag(string $name, mixed $keys = null, bool $overlay = false) static 缓存标签
    * @method object handler() static 返回句柄对象,可执行其它高级方法
    */
    class Cache extends Facade {
     /**
      * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
      * @access protected
      * @return string
      */
     protected static function getFacadeClass() {
         return 'cache';
     }
    }
    

    分析
    所有继承自 Facade 的类都可以静态的调用对象,但是如果这个类与 Facade 都没有该方法,那么按照PHP的特性,应该有个魔术方法 __callStatic() 来处理静态调用不到方法的情况,和生成一个目标类的对象的处理(这里衍生出来一个问题,目标类如何指定)。
    查看第一层源码发现只有一个方法:

    <?
    protected static function getFacadeClass() {
     return 'cache';
    }
    

    猜想

  3. 这有可能就是指定目标类的语句,然后在父类进行实例化(根据依赖倒转原则,在父类中使用了子类必须重写的方法来获取指定的依赖名称)。

  4. 魔术方法 __callStatic() 和类对象的处理这里没有实现的代码,那么应该可能在父类实现了,毕竟 Facade 是个通用的类。

继续往里面看。

2.2 查看Facade源码

<?php
namespace think;

class Facade {
    ...

    /**
     * 创建Facade实例
     * @static
     * @access protected
     * @param  string    $class          类名或标识
     * @param  array     $args           变量
     * @param  bool      $newInstance    是否每次创建新的实例
     * @return object
     */
    protected static function createFacade($class = '', $args = [], $newInstance = false) {
        $class = $class ?: static::class;

        $facadeClass = static::getFacadeClass();

        if ($facadeClass) {
            $class = $facadeClass;
        } elseif (isset(self::$bind[$class])) {
            $class = self::$bind[$class];
        }

        if (static::$alwaysNewInstance) {
            $newInstance = true;
        }

        return Container::getInstance()->make($class, $args, $newInstance);
    }

    /**
     * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
     * @access protected
     * @return string
     */
    protected static function getFacadeClass() {}

    ...

    // 调用实际类的方法
    public static function __callStatic($method, $params) {
        return call_user_func_array([static::createFacade(), $method], $params);
    }
}

验证

  1. 果不其然有个 __callStatic() 方法。
  2. 有个 getFacadeClass() 方法,但是这与猜想不同,不是抽象方法(Facade类也不是抽象类)。

分析

  1. 魔术方法 __callStatic() 直接返回一个函数 call_user_func_array 的调用结果。那么按照 call_user_func_array 的功能来说,是对方法的调用具体实现了。

    <?
    public static function __callStatic($method, $params) {
     return call_user_func_array([static::createFacade(), $method], $params);
    }
    
  2. 查看 static::createFacade()

    <?
    protected static function createFacade($class = '', $args = [], $newInstance = false) {
     ...
    
     $facadeClass = static::getFacadeClass();
    
     ...
    
       // 主要生成逻辑
     return Container::getInstance()->make($class, $args, $newInstance);
    }
    

    注意:这时已经跳转到了 Container 类的处理了。

  3. 查看 Container::getInstance()->make()

    <?
    public function make($abstract, $vars = [], $newInstance = false) {
       ...
    
     // 主要生成逻辑
     $object = $this->invokeClass($abstract, $vars);
    
     ...
    
     return $object;
    }
    
  4. 查看 $object = $this->invokeClass($abstract, $vars);

    <?
    public function invokeClass($class, $vars = []) {
     try {
         $reflect = new ReflectionClass($class);
    
           // 首先检查是否有 __make() 方法
         if ($reflect->hasMethod('__make')) {
             $method = new ReflectionMethod($class, '__make');
    
             if ($method->isPublic() && $method->isStatic()) {
                 $args = $this->bindParams($method, $vars);
                 return $method->invokeArgs(null, $args);
             }
         }
    
           // 通过构造函数生成
         $constructor = $reflect->getConstructor();
    
         $args = $constructor ? $this->bindParams($constructor, $vars) : [];
    
         return $reflect->newInstanceArgs($args);
    
     } catch (ReflectionException $e) {
         throw new ClassNotFoundException('class not exists: ' . $class, $class);
     }
    }
    

    这时,进入方法 invokeClass() 方法的参数: $class = 'cache'$vars = []
    反射首先检查类 Cache 是否有 __make() 方法,同时该方法是 public static 的,如果是,这返回该方法的生成结果。
    分析:这个 public static funtion __make() 方法应该是框架通过容器类 Container 自动生成对象时的一个提供给用户使用的自定义初始化的入口:
    image.png
    这时流程回到了类 Cache__make() 方法,因为缓存类有__make()方法。

    2.3 查看Cache源码

    <?
    namespace think;
    class Cache {
         ...
    
     public function __construct(array $config = []) {
         $this->config = $config;
         $this->init($config);
     }
    
     /**
      * 连接缓存
      * @access public
      * @param  array         $options  配置数组
      * @param  bool|string   $name 缓存连接标识 true 强制重新连接
      * @return Driver
      */
     public function connect(array $options = [], $name = false) {
         if (false === $name) {
             $name = md5(serialize($options));
         }
    
         if (true === $name || !isset($this->instance[$name])) {
             $type = !empty($options['type']) ? $options['type'] : 'File';
    
             if (true === $name) {
                 $name = md5(serialize($options));
             }
    
             $this->instance[$name] = Loader::factory($type, '\\think\\cache\\driver\\', $options);
         }
    
         return $this->instance[$name];
     }
    
     /**
      * 自动初始化缓存
      * @access public
      * @param  array         $options  配置数组
      * @param  bool          $force    强制更新
      * @return Driver
      */
     public function init(array $options = [], $force = false) {
           ...
    
         $this->handler = $this->connect($options);
    
         return $this->handler;
     }
    
     public static function __make(Config $config) {
         return new static($config->pull('cache'));
     }
    }
    

    分析:由于 __make() 接管了容器初始化 Cache 对象的步骤,则这里必须要 new 出来一个 Cache。

  5. 读取配置,初始化 Cache ,连接缓存: ```php <? public function __construct(array $config = []) { $this->config = $config; $this->init($config); }

public function init(array $options = [], $force = false) { …

$this->handler = $this->connect($options);

return $this->handler;

}

public static function __make(Config $config) { return new static($config->pull(‘cache’)); }


2. 连接缓存 `connect()` :
```php
<?
public function connect(array $options = [], $name = false) {
    if (false === $name) {
        $name = md5(serialize($options));
    }

    if (true === $name || !isset($this->instance[$name])) {
        $type = !empty($options['type']) ? $options['type'] : 'File';

        if (true === $name) {
            $name = md5(serialize($options));
        }

          // 加载依赖 $type
        $this->instance[$name] = Loader::factory($type, '\\think\\cache\\driver\\', $options);
    }

    return $this->instance[$name];
}

分析

  1. 连接缓存方法需要加载依赖 $type 来使用,而且还是返回这个依赖,也就是说 __make() 方法初始化 Cache 的结果就是返回这个依赖,说明,Cache是对 $type 类的再封装,如果回到开始的 Cache::set() 调用, set() 方法使用的是 $type 类的属性。
  2. $options 内容是由 __make() 方法加载到的配置,这个配置是:
    <?php
    return [
     // 驱动方式
     'type'   => 'File',
     // 缓存保存目录
     'path'   => '',
     // 缓存前缀
     'prefix' => '',
     // 缓存有效期 0表示永久缓存
     'expire' => 0,
    ];
    
    也就是说 $type 指向的是 File 的类。查看 File 类可以看到对应的 set() 方法等对应的在 facade\cache() 类的注释中出现的方法了。
    至此,这个 facade\cache() 的初始化及调用过程就清楚了。

    3. 总结

    TP5门面模式的设计流程图如下:

image.png