Thinkphp钩子Hook的工作笔记

作者:袖梨 2022-06-25

之前写到TP3.1的行为扩展是tag();在TP3.2中引入了另一种说法—:钩子。

我们来看一下TP3.2中的钩子这个东西:

一:文件流程:

1:/index.php ->require './ThinkPHP/ThinkPHP.php';

2:/ThinkPHP/ThinkPHP.php—->require CORE_PATH.'Think'.EXT; ThinkThink::start();

3:/ThinkPHP/Library/Think/Think.class.php—–>App::run();

4:/ThinkPHP/Library/Think/App.class.php 。到这里基本流程就走完了,(这里不说细节);

首先,我们要明确一些说法。在TP中,设置陷阱的过程称为##绑定事件##,而某个事件触发的功能函数称为##行为##。

钩子应该具有的基本方法应该有:

设置钩子(导入钩子)
触发事件
执行行为
首先我们看看TP是怎么写的,源代码位于ThinkPHP/Library/Think/Hook.class.php,Hook类中全是静态方法,其中有唯一静态属性$tags,他是一个数组,键为绑定的事件,值为绑定的行为。

其中有两个方法可以用于绑定,前者是单个,后者是是批量。

    static public function add($tag,$name) {
        echo $tag;
        echo "n";
        if(!isset(self::$tags[$tag])){
            self::$tags[$tag]   =   array();
        }
        if(is_array($name)){
            self::$tags[$tag]   =   array_merge(self::$tags[$tag],$name);
        }else{
            self::$tags[$tag][] =   $name;
        }
    }

    static public function import($data,$recursive=true) {
        if(!$recursive){ // 覆盖导入
            self::$tags   =   array_merge(self::$tags,$data);
        }else{ // 合并导入
            foreach ($data as $tag=>$val){
                if(!isset(self::$tags[$tag]))
                    self::$tags[$tag]   =   array();           
                if(!empty($val['_overlay'])){
                    // 可以针对某个标签指定覆盖模式
                    unset($val['_overlay']);
                    self::$tags[$tag]   =   $val;
                }else{
                    // 合并模式
                    self::$tags[$tag]   =   array_merge(self::$tags[$tag],$val);
                }
            }           
        }
    }
当系统触发了某个事件,比如app_start事件,TP会找到Hook::listen方法,该方法会查找$tags中有没有绑定app_start事件的方法,然后用foreach遍历$tags属性,并执行Hook:exec方法。

    static public function listen($tag, &$params=NULL) {
        if(isset(self::$tags[$tag])) {
            if(APP_DEBUG) {
                G($tag.'Start');
                trace('[ '.$tag.' ] --START--','','INFO');
            }
            foreach (self::$tags[$tag] as $name) {
                APP_DEBUG && G($name.'_start');
                $result =   self::exec($name, $tag,$params);
                if(APP_DEBUG){
                    G($name.'_end');
                    trace('Run '.$name.' [ RunTime:'.G($name.'_start',$name.'_end',6).'s ]','','INFO');
                }
                if(false === $result) {
                    // 如果返回false 则中断插件执行
                    return ;
                }
            }
            if(APP_DEBUG) { // 记录行为的执行日志
                trace('[ '.$tag.' ] --END-- [ RunTime:'.G($tag.'Start',$tag.'End',6).'s ]','','INFO');
            }
        }
        return;
    }
Hook:exec方法会检查行为名称,如果包含Behavior关键字,那么入口方法必须为run方法,而执行run方法的参数在调用Hook::listen时指定。但如果不用##Behavior##关键字做配置,即将系统默认的ReadHtmlCacheBehavior改为ReadHtml,系统会报错吗?答案是会的!

如果去掉Behavior,系统就会找该类中绑定事件名称的方法,即app_begin。这样的好处是,不会强制使用run方法,一个行为可以复用了。

    static public function exec($name, $tag,&$params=NULL) {
        if('Behavior' == substr($name,-8) ){
            // 行为扩展必须用run入口方法
            $tag    =   'run';
        }
        $addon   = new $name();
        return $addon->$tag($params);
    }

补充:

Thinkphp提供了一个钩子机制,Thinkphp提供了一些载点,可以让用户在相应的时机处理一些用户自己的扩展。 例如app_begin,action_begin,view_begin等。 Hook的配置写在tags.php中

return array(
    'app_begin' => array(
        'BehaviorCheckLangBehavior'
    ),
    'action_begin' => array(
        'HomeBehaviorsLocateBehavior',
        'HomeBehaviorsTokenBehavior',
        'HomeBehaviorsCookieBehavior'
    ),
    'view_begin' => array(
        'HomeBehaviorsSeoBehavior',
        'HomeBehaviorsParseBehavior'
    ),
    'view_end'=>array(
        'HomeBehaviorsSlowLogBehavior',
    ),
);
例如上面这个配置文件使用了4个载点,分别在下面挂上了自己的behavior扩展。 这样thinkphp在执行到加载控制器方法action_begin时,就会先执行用户自己定义的三个扩展

‘HomeBehaviorsLocateBehavior’, ‘HomeBehaviorsTokenBehavior’, ‘HomeBehaviorsCookieBehavior’

下面让我们来详细看看Hook执行的流程。

首先,在Think.class.php中Think::start()方法中

          // 加载应用行为定义
          if(is_file(CONF_PATH.'tags.php'))
          // 允许应用增加开发模式配置定义
          Hook::import(include CONF_PATH.'tags.php');  
thinkPHP加载了tags.php配置文件,从中读取用户的载点配置。通过Hook:import加载。

其次,在App.class.php中,App:run()方法中,Thinkphp监听了app_init,app_begin,app_end三个载点。

   static public function run() {
        // 应用初始化标签
        Hook::listen('app_init');
        App::init();
        // 应用开始标签
        Hook::listen('app_begin');
        // Session初始化
        if(!IS_CLI){
            session(C('SESSION_OPTIONS'));
        }
        // 记录应用初始化时间
        G('initTime');
        App::exec();
        // 应用结束标签
        Hook::listen('app_end');
        return ;
    }
Hook通过Hook::listen这个方法来监听载点。我们来看看这个方法是怎么写的。

static public function listen($tag, &$params=NULL) {
        if(isset(self::$tags[$tag])) {
            if(APP_DEBUG) {
                G($tag.'Start');
                trace('[ '.$tag.' ] --START--','','INFO');
            }
            foreach (self::$tags[$tag] as $name) {
                APP_DEBUG && G($name.'_start');
                $result =   self::exec($name, $tag,$params);
                if(APP_DEBUG){
                    G($name.'_end');
                    trace('Run '.$name.' [ RunTime:'.G($name.'_start',$name.'_end',6).'s ]','','INFO');
                }
                if(false === $result) {
                    // 如果返回false 则中断插件执行
                    return ;
                }
            }
            if(APP_DEBUG) { // 记录行为的执行日志
                trace('[ '.$tag.' ] --END-- [ RunTime:'.G($tag.'Start',$tag.'End',6).'s ]','','INFO');
            }
        }
        return;
    }
$tags是通过Hook::import()加载进来的用户载点配置 Hook通过循环配置项来检测载点下是否有用户的扩展需要执行,通过self::exec($name, $tag,$params);来执行。 需要注意一下Hook::exec()这个方法。

static public function exec($name, $tag,&$params=NULL) {
        if('Behavior' == substr($name,-8) ){
            // 行为扩展必须用run入口方法
            $tag    =   'run';
        }
        $addon   = new $name();
        return $addon->$tag($params);
    }
这里规定了如果用户加载的是行为扩展,即以Behevior结尾,必须用run方法作为入口。

下面我们再来看看其他的载点又是如何定义的

View.class.php中的View::display()方法中定义了view_begin,view_end两个载点。

public function display($templateFile='',$charset='',$contentType='',$content='',$prefix='') {
    G('viewStartTime');
    // 视图开始标签
    Hook::listen('view_begin',$templateFile);
    // 解析并获取模板内容
    $content = $this->fetch($templateFile,$content,$prefix);
    // 输出模板内容
    $this->render($content,$charset,$contentType);
    // 视图结束标签
    Hook::listen('view_end');
}
View::fetch()中定义了view_parse与view_filter载点。

public function fetch($templateFile='',$content='',$prefix='') {
    if(empty($content)) {
        $templateFile   =   $this->parseTemplate($templateFile);
        // 模板文件不存在直接返回
        if(!is_file($templateFile)) E(L('_TEMPLATE_NOT_EXIST_').':'.$templateFile);
    }
    // 页面缓存
    ob_start();
    ob_implicit_flush(0);
    if('php' == strtolower(C('TMPL_ENGINE_TYPE'))) { // 使用PHP原生模板
        $_content   =   $content;
        // 模板阵列变量分解成为独立变量
        extract($this->tVar, EXTR_OVERWRITE);
        // 直接载入PHP模板
        empty($_content)?include $templateFile:eval('?>'.$_content);
    }else{
        // 视图解析标签
        $params = array('var'=>$this->tVar,'file'=>$templateFile,'content'=>$content,'prefix'=>$prefix);
        Hook::listen('view_parse',$params);
    }
    // 获取并清空缓存
    $content = ob_get_clean();
    // 内容过滤标签
    Hook::listen('view_filter',$content);
    // 输出模板文件
    return $content;
}
Controller.class.php的构造方法中监听了action_begin,析构函数中监听了action_end。

public function __construct() {
        Hook::listen('action_begin',$this->config);
        //实例化视图类
        $this->view     = Think::instance('ThinkView');
        //控制器初始化
        if(method_exists($this,'_initialize'))
            $this->_initialize();
    }
ajaxReturn方法中监听了ajax_return。

Controller方法中我们可以看到thinkphp是如何在控制器中加入assign方法的。

  $this->view     = Think::instance('ThinkView');
  protected function assign($name,$value='') {
        $this->view->assign($name,$value);
        return $this;
  }
同样的方法我们也可以在我们的behavior扩展中加入assign方法。

相关文章

精彩推荐