Thinkphp源码分析系列(五)–系统钩子实现

Thinkphp的插件机制主要依靠的是Hook.class.php这个类,官方文档中在行为扩展也主要依靠这个类来实现。下面我们来具体看看tp是怎么利用这个类来实现行为扩展的。

首先,行为扩展是什么?有wordpress二次开发经验的同学应该很容易明白,其实就是钩子,tp在其内核的执行过程中内置了诸多钩子,这些钩子可以允许我们能够在不改变内核代码的基础上来对内核进行一定程度的修改。tp的钩子机制的实现类就是Hook.class.php。

Hook.class.php内部维护了一个数组,这个数组的键就是钩子的名称,值就是类的名称的集合。我们利用Hook类的add方法可以添加一个钩子,其实就是往这个维护的数组上添加一个键值。tp默认已经定义了很多钩子标签。

app_init 应用初始化标签位
path_info PATH_INFO检测标签位
app_begin 应用开始标签位
action_name 操作方法名标签位
action_begin 控制器开始标签位
view_begin 视图输出开始标签位
view_parse 视图解析标签位
template_filter 模板内容解析标签位
view_filter 视图输出过滤标签位
view_end 视图输出结束标签位
action_end 控制器结束标签位
app_end 应用结束标签位

在3.2版本的tp框架中,钩子标签的实现机制是这样的。

首先所有的钩子标签和其对应的类是记录在应用模式文件中。tp默认的应用模式是common,对应的应用模式文件是Thinkphp/Mode/Common.php文件。在此文件中我们可以看到行为扩展的定义:

// 行为扩展定义
'tags' => array(
'app_init' => array(
'BehaviorBuildLiteBehavior', // 生成运行Lite文件
),
'app_begin' => array(
'BehaviorReadHtmlCacheBehavior', // 读取静态缓存
),
'app_end' => array(
'BehaviorShowPageTraceBehavior', // 页面Trace显示
),
'view_parse' => array(
'BehaviorParseTemplateBehavior', // 模板解析 支持PHP、内置模板引擎和第三方模板引擎
),
'template_filter'=> array(
'BehaviorContentReplaceBehavior', // 模板输出替换
),
'view_filter' => array(
'BehaviorWriteHtmlCacheBehavior', // 写入静态缓存
),
),

我们在前面说到 ThinkPHP 引导类的时候讲到此类会根据当前的模式读取模式文件并且按照模式文件中的配置依次去读取配置文件从而完成系统核心的加载。其中有一项就是上面的行为模式。在这个引导类的75行左右,程序开始加载模式文件中定义的标签和类,通过Hook::import方法把这些标签和类的映射加载到了Hook内部为的tags数组中。

// 加载模式别名定义
if(isset($mode['alias'])){
self::addMap(is_array($mode['alias'])?$mode['alias']:include $mode['alias']);
}

然后在后面我们就可以看到tp框架在监听这些标签。何为监听?我们来看一下APP.class.php里面使用到的监听。

/**
* 运行应用实例 入口文件使用的快捷方法
* @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::listen(‘app_begin’);就是一个监听。当程序执行到此处代码的时候,这个代码会去执行listen方法,此方法会去检测Hook持有的tags数组中是否含有app_begin标签,如果有的话就去看其对应的类文件,并到ThinkPHPLibraryBehavior目录下去寻找对应的类文件并加载实例化。然后就去调用实例化对象的run方法并执行。

由此可见,如果我们想要在应用执行开始的时候加一些我们自己的实现逻辑,只需要写一个带有run方法的行为类,这个类一般继承自Behavior类,然后在run方法中写入自己的逻辑,然后把我们写好的类名加到模式文件中,这样就可以轻松的做到扩展核心代码了。

下面我们来具体看一下Hook类的实现细节。

// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2013 http://topthink.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace Think;
/**
 * ThinkPHP系统钩子实现
 */
class Hook {
 //这就是Hook类持有的tags静态数组变量,此变量以键值对的形式存储标签和类的映射。
 static private $tags = array();

 /**
 * 动态添加插件到某个标签
 * @param string $tag 标签名称
 * @param mixed $name 插件名称
 * @return void
  其实就是把$tag作为键,$name 作为对应的额值加入到tags数组中。如果$name是一个数组就合并到tags中。
 */
 static public function add($tag,$name) {
 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;
 }
 }

 /**
 * 批量导入插件
 * @param array $data 插件信息
 * @param boolean $recursive 是否递归合并
 * @return void
  导入插件的本质还是把数据加入到tags数组中。但是其传入的参数是$data数组,$data本身就是一个类似于tags的东西,它存储的也是标签和类的映射。所以是把$data和$tags合并了。
 */
 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);
 }
 }
 }
 }

 /**
 * 获取插件信息
 * @param string $tag 插件位置 留空获取全部
 * @return array
 */
 static public function get($tag='') {
 if(empty($tag)){
 // 获取全部的插件信息
 return self::$tags;
 }else{
 return self::$tags[$tag];
 }
 }

 /**
 * 监听标签的插件
 * @param string $tag 标签名称
 * @param mixed $params 传入参数
 * @return void
  此函数最为重要,其中调用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;
 }

 /**
 * 执行某个插件
 * @param string $name 插件名称
 * @param string $tag 方法名(标签名)
 * @param Mixed $params 传入的参数
 * @return void
  执行插件的原理:其实就是通过标签从tags数组中得到类名的集合,然后拼凑出类文件名称,实例化类,执行类的run方法。
 */
 static public function exec($name, $tag,&$params=NULL) {
 if('Behavior' == substr($name,-8) ){
 // 行为扩展必须用run入口方法
 $tag = 'run';
 }
 $addon = new $name();
 return $addon->$tag($params);
 }
}

 加入博主个人博客,一起学习 http://www.kanronghua.com/

原文地址:https://www.cnblogs.com/ronghua/p/6006801.html