周梦康 发表于 2014-05-11 8104 次浏览 标签 : OneThink

下面的笔记,自己再看觉得好二,但是有时候大脑就是会短路,反过来想是顺理成章,实际最开始不熟悉的时候,难免会走些之后看起来很傻很二的弯路,错路。

本人脸皮厚,分析错了的地方,也就不删了,不装高智商。

以OneThink的后台首页为例:分析OneThink的钩子的整体运作流程,它到底是怎么样的一个机制 [基本废话,旁人可无视]

这里有其自定一个的标签hook里面传了一个AdminIndex作为参数,想必是调用了AdminIndex的方法什么的,感觉全局搜索应该会有对应的代码。搜索结果如图:

三个控制器里面有AdminIndex这个方法,还有一条SQL与之相关。

摘出一个控制器的代码:

class DevTeamAddon extends Addon{
    //实现的AdminIndex钩子方法
    public function AdminIndex($param){
        $config = $this->getConfig();
        $this->assign('addons_config', $config);
        if($config['display'])
            $this->display('widget');
    }
}

SQL语句为:

INSERT INTO `onethink_hooks` VALUES ('13', 'AdminIndex', '首页小格子个性化显示', '1', '1382596073', 'SiteStat,SystemInfo,DevTeam');

补充说明:表字段依次为id,name,description,type,update_time,addons

根据AdminIndex里面的代码,我去找其父类的getConfig的方法。按住Command(windows下安转Ctrl),点击跳转到父类方法:

/**
 * 获取插件的配置数组
 */
final public function getConfig($name=''){
    static $_config = array();
    if(empty($name)){
        $name = $this->getName();
    }
    if(isset($_config[$name])){
        return $_config[$name];
    }
    $config =   array();
    $map['name']    =   $name;
    $map['status']  =   1;
    $config  =   M('Addons')->where($map)->getField('config');
    if($config){
        $config   =   json_decode($config, true);
    }else{
        $temp_arr = include $this->config_file;
        foreach ($temp_arr as $key => $value) {
            if($value['type'] == 'group'){
                foreach ($value['options'] as $gkey => $gvalue) {
                    foreach ($gvalue['options'] as $ikey => $ivalue) {
                        $config[$ikey] = $ivalue['value'];
                    }
                }
            }else{
                $config[$key] = $temp_arr[$key]['value'];
            }
        }
    }
    $_config[$name]     =   $config;
    return $config;
}

代码比较简单,首先看静态变量$_config里有没有,没有就去数据里找,如果数据库里也没有,就去看这个插件目录下面的配置文件。发现这似乎是与钩子需要实现的插件相关的代码,似乎与钩子无关。

不禁想,既然AdminIndex也不是实现其父类的方法,那么钩子是如何找到的呢?我想是不是应该从刚刚搜出来的数据库入手。因为数据库里存了一个钩子的name=>AdminIndex。这里还需要先去看看

{:hook('AdminIndex')}

经过编译之后到底是什么样子,这个{:hook()}到底是个什么函数呢?,还是搜AdminIndex,查看onethink/Application/Admin/View/Index/index.html编译之后的文件发现是echo hook('AdminIndex'),如图所示:

点击进去,查看hook这个函数

跳转到function.php查看其代码为:

/**
 * 处理插件钩子
 * @param string $hook   钩子名称
 * @param mixed $params 传入参数
 * @return void
 */
function hook($hook,$params=array()){
    \Think\Hook::listen($hook,$params);
}

顺藤摸瓜,找到onethink/ThinkPHP/Library/Think/Hook.class.php里面的listen方法

/**
 * 监听标签的插件
 * @param string $tag 标签名称
 * @param mixed $params 传入参数
 * @return void
 */
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;
}

把debug代码去掉,就一点点:

static public function listen($tag, &$params=NULL) {
    if(isset(self::$tags[$tag])) {
        foreach (self::$tags[$tag] as $name) {
            $result =   self::exec($name, $tag,$params);
        }
    }
    return;
}

然后找里面的self::exec方法

/**
 * 执行某个插件
 * @param string $name 插件名称
 * @param string $tag 方法名(标签名)     
 * @param Mixed $params 传入的参数
 * @return void
 */
static public function exec($name, $tag,&$params=NULL) {
    if(false === strpos($name,'\\')) {
        // 插件(多个入口)
        $class   =  "Addons\\{$name}\\{$name}Addon";
    }else{
        // 行为扩展(只有一个run入口方法)
        $class   =  $name.'Behavior';
        $tag    =   'run';
    }
    $addon   = new $class();
    return $addon->$tag($params);
}

哦,原来钩子里不仅可以挂插件,也可以挂行为扩展。这里还是以插件为例子。后台首页的onethink/Application/Admin/View/Index/index.html里执行了hook('AdminIndex'),它里面执行了Hook::listen('AdminIndex'),继续执行了Hook::exec($name,'AdminIndex')

通过第一张截图可以知道AdminIndex这个钩子在三个文件/Addons/DevTeam/SiteStat.class.php,/Addons/DevTeam/SystemInfo.class.php,/Addons/DevTeam/DevTeamAddon.class.php中被实现。也就是调用Hook::listen()方法的时候,里面的静态成员self::$tags[$tag]

array('SiteStat','SystemInfo','DevTeam')

这样在Hook::exec()方法里面就可以找到Addons\\SiteStat\\SiteStatAddon

现在剩下的任务就是只有一个了,要弄清楚这个self::$tags[$tag]是如何初始化的。

想了好一会,又去刷了会微博,实在没想到,但是这个数据的初始化应该是从前面说的那个数据库的onethink_hooks表里查询出来的。所以我尝试着搜下M('Hooks')(虽然这是一个很笨的办法)。看了下找到的内容,似乎两个控制器里面的代码都不是我所想看到的。

没道理啊,由于这里是静态方法,静态成员,也不存在实例化的问题,在这个类里面也没有看到初始化的代码,唯一的可能就是系统初始化的时候从数据库里加载里已有的钩子。但是为什么搜M('Hooks')搜不到呢?

我又从ThinkPHP.php往下找,看到Think/Think::start()里面有很多初始化的内容,里面有Hooks::import()

// 加载模式行为定义
if(isset($mode['tags'])) {
  Hook::import(is_array($mode['tags'])?$mode['tags']:include $mode['tags']);
}

而这里导入的都是onethink/ThinkPHP/Mode/common.php里面的数组里面的行为扩展

// 行为扩展定义
'tags'  =>  array(
    'app_begin'     =>  array(
        'Behavior\ReadHtmlCache', // 读取静态缓存
    ),
    'app_end'       =>  array(
        'Behavior\ShowPageTrace', // 页面Trace显示
    ),
    'view_parse'    =>  array(
        'Behavior\ParseTemplate', // 模板解析 支持PHP、内置模板引擎和第三方模板引擎
    ),
    'template_filter'=> array(
        'Behavior\ContentReplace', // 模板输出替换
    ),
    'view_filter'   =>  array(
        'Behavior\WriteHtmlCache', // 写入静态缓存
    ),
),

好吧,再看Think/Think::start()里面还有一处使用Hooks::import()

Hook::import(include CONF_PATH.'tags.php');
///文件地址onethink/Application/Common/Conf/tags.php

里面包含的是一个数组

array(
	'app_init'=>array('Common\Behavior\InitHook')
);

这样在执行完这个导入之后与原有的数据做array_mergeHook::$tags的数据为:

array(6) {
  ["app_begin"] => array(1) {
    [0] => string(22) "Behavior\ReadHtmlCache"
  }
  ["app_end"] => array(1) {
    [0] => string(22) "Behavior\ShowPageTrace"
  }
  ["view_parse"] => array(1) {
    [0] => string(22) "Behavior\ParseTemplate"
  }
  ["template_filter"] => array(1) {
    [0] => string(23) "Behavior\ContentReplace"
  }
  ["view_filter"] => array(1) {
    [0] => string(23) "Behavior\WriteHtmlCache"
  }
  ["app_init"] => array(1) {
    [0] => string(24) "Common\Behavior\InitHook"
  }
}

没错应该就是从这里入手,从名字上来看就是InitHook相关的文件了。其实前面刚刚搜M('Hooks')的时候有搜出来,没仔细看。就是InitHookBehavior.class.php这个文件,走到这个文件就是初始化的最后一步了,之前还有一步,就是如何执行的Common\Behavior\InitHookBehavior::run(),根据前面的经验,既然在Hook::$tags里面有app_init的键,所以只需搜下,即可发现

原来在\Think\App::run()的最开始做了钩子的初始化(执行Hook::listen('app_init')

/**
 * 运行应用实例 入口文件使用的快捷方法
 * @access public
 * @return void
 */
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::exec('Common\Behavior\InitHook'),根据这样就实例化Common\Behavior\InitHookBehavior(),然后调用了其run()方法。也就是说在我们实例化一个app的时候,它先用钩子的监听方法,触发了所有钩子的初始化,将所有钩子数据放入了Hook::$tags里面。下面是Common\Behavior\InitHookBehavior()的代码:

// 初始化钩子信息
class InitHookBehavior extends Behavior {

    // 行为扩展的执行入口必须是run
    public function run(&$content){
        if(isset($_GET['m']) && $_GET['m'] === 'Install') return;
        
        $data = S('hooks');
        if(!$data){
            $hooks = M('Hooks')->getField('name,addons');
            foreach ($hooks as $key => $value) {
                if($value){
                    $map['status']  =   1;
                    $names          =   explode(',',$value);
                    $map['name']    =   array('IN',$names);
                    $data = M('Addons')->where($map)->getField('id,name');
                    if($data){
                        $addons = array_intersect($names, $data);
                        Hook::add($key,$addons);
                    }
                }
            }
            S('hooks',Hook::get());
        }else{
            Hook::import($data,false);
        }
    }
}

这样也验证了前面对初始化的时候做M('Hooks')取数据的猜想,前面虽然搜到了这个文件,但没认真看。怎么把数据库里的钩子挂载的插件用逗号切割,也都在里面了。这样就在后面使用钩子函数执行hook('AdminIndex')的时候,Hook::$tags里面已经有对应的数据了。

  ["AdminIndex"] => array(3) {
    [0] => string(8) "SiteStat"
    [1] => string(10) "SystemInfo"
    [2] => string(7) "DevTeam"
  }


👇 下面是我的公众号,高质量的博文我会第一时间同步到公众号,给个关注吧!

评论列表