2、基于反射机制:实现钩子机制

钩子是编程里一个常见的概念,非常的重要。 它可以让程序变得非常容易扩展(而不用理解其内部的实现机理,这样可以减少很多工作量)。
只要有一个钩子样本,能很容易仿照第一个钩子快速的编写出第二个钩子,
(钩子是插件的原型,只有先理解钩子的实现原理,才有可能掌握插件机制)
下面我们通过一个简单的案例对钩子进行一个简单的理解:

  1. <?php
  2. class Test {
  3. public static function example() {
  4. $arr = [1, 2];
  5. echo '我是一个钩子测试<br/>';
  6. echo 'hello<br/>';
  7. var_dump($arr);
  8. }
  9. }
  10. Test::example();

一个Test类里面,写了一个example()方法。
本来这个example()的方法非常简单,就是输出hello,
但是在这之前,我们刚好还有其他的事情要做
(例如:假定在输入hello之前,有一个字符串要输出,并且在之后有个数组要打印)。
那么我们现在有2种实现写法:
1、直接在成员函数里实现我们需要的功能(就像上面的案例代码那样);
但是这种方式有个问题,就是我们每次更改程序,都需要去更改系统的核心部分(我们假定Test是系统的核心部分),这样会需要我们每次改动都要跳到Test类内部去改动,开发成本非常大,而且代码全部在一起非常不好维护。
2、我们封装一个execute()自定义函数(注意不是class的成员函数),只需要调用这个函数就相当于触发程序执行:

  1. <?php
  2. function execute($params) {
  3. if(is_array($params)){
  4. var_dump($params);
  5. }else{
  6. echo $params;
  7. }
  8. }
  9. class Test {
  10. public static function example() {
  11. execute('我是一个钩子测试<br/>');
  12. echo 'hello<br/>';
  13. execute([1, 2]);
  14. }
  15. }
  16. Test::example();

但是现在仍然有个问题,我们改动的时候,仍然要去系统内部改动
(如果是简单的数组和字符串,是可以进行配置,但是如果是复杂的逻辑处理,那么单靠配置execute()函数肯定行不通)。
这时候我们可以设计一个类,这个类实现了execute()函数的钩子触发、配置逻辑分离的功能,
通过这个类,向系统发送消息的时候,系统可以直接调用我们的类,
而且我们的类只要遵循一定的规则来设计,就和原系统是相容的。
做了改进后的钩子格式代码如下:

<?php
# 钩子类
class Hook {
    /**
     * 触发钩子
     * @param string $type  事件类型
     * @param string $model 触发对象
    */
    static public function execute($type, $model) {
        $obj = new $model();
        if ($type == 'string') {
            $obj->string();
        } elseif ($type == 'array') {
            $obj->array();
        }
    }
}
# 钩子对象类
class Hello {
    public function string() {
        $str = '我是一个钩子测试<br>';
        echo $str;
    }
    public function array() {
        var_dump([1, 2]);
    }
}
# 测试类
class Test {
    public static function example() {
        Hook::execute('string', 'Hello');
        echo 'hello<br/>';
        Hook::execute('array', 'Hello');
    }
}
Test::example();

从上面可以看出,组成一个单独的类,系统的内部固定了后,外部可以写各种类,进行钩子的实现。
现在写了一个Hello类,假如需要拓展一个World类,同样可以仅仅改动Hook内部,增加更多的构造行为, 而不用去改动Test内部,只要我们定义一个抽象类:

<?php
abstract class Behavior{
    abstract public function string();
    abstract public function array();
}

然后让所有的拓展类,比如Hello类或者World类继承这个抽象类,就可以直接编写更多的扩展。
可能很多朋友到这里会有些疑惑?
上面所说的案例都是跟反射机制无关的,那使用反射机制应该怎么实现钩子自动触发呢?
别急,下面我们来使用反射机制,实现钩子功能:

1、假设,我们Hook类下面有一个plugins目录,里面每一个类,就是一个钩子,而Hook类会自动监听这些钩子。
2、当使用Hook类的execute()成员函数时,就可以触发对应的钩子。
<?php
class Hook{
    private $_rootDir;
    private $_Array = [];
    /** 
     * 构造函数
    */
    public function __construct() {
        $this->_rootDir = dirname(__FILE__) . DIRECTORY_SEPARATOR .'plugins'. DIRECTORY_SEPARATOR;
        # 获得插件目录
        $this->directory($this->_rootDir);
    }
    /**
     * 获得插件的名称
    */
    public function directory($dir_name){
        $dir_handle = opendir($dir_name);
        if (!$dir_handle) {
            die ('目录打开错误!');
        }
        # 文件名为'0'时,readdir返回FALSE,判断返回值是否不全等
        while (false!==($filename=readdir($dir_handle))) {
            if ($filename!='.' && $filename!='..') {
                $file = ltrim($dir_name, $this->_rootDir) . $filename;
                $this->_Array[] = trim($file, '.php');
            }
        }
        # 关闭目录句柄
        closedir($dir_handle);
    }
    /** 
     * 触发一个钩子 
     * 
     * @param string $hook 钩子的名称 
     * @param mixed $type 动态调用钩子内的方法
     * @param array $data 钩子的入参 
     * @return mixed 
    */
    public function trigger($hook, $type, $data=[]) { 
        if (!in_array($hook, $this->_Array)) {
            die('钩子不存在');
        }
        # 引入钩子类
        require_once $this->_rootDir.$hook.'.php';
        $class = new ReflectionClass($hook);
        if ($class->hasMethod($type) == false) {
            die ('钩子对应触发的函数不存在');
        }
        # 实例化钩子
        $class = new ReflectionMethod($hook, $type);
        # 触发钩子对应的成员函数,并传参
        $class->invokeArgs(new $hook(), $data);
    }     
}
# 实例化Hook核心类
$plugin = new Hook();
# 触发一个钩子
$plugin->trigger('Order', 'Hello');
# 再触发一个钩子
$plugin->trigger('User', 'Ri', ['你妹啊', '我日']);

上面的案例是我们的核心代码,如果想尝试运行的朋友可以下载完整代码后再运行: 下载
相对于不使用反射机制实现的钩子触发功能,这样的实现方式是不是要灵活许多,
基于这种实现方式的钩子机制还能升级成插件机制,实现许多插件扩展的功能。