之前写到TP3.1的行为扩展是tag();在TP3.2中引入了另一种说法—:钩子。
我们来看一下TP3.2中的钩子这个东西:
一:文件流程:
1:/index.php ->require './ThinkPHP/ThinkPHP.php';
2:/ThinkPHP/ThinkPHP.php—->require CORE_PATH.'Think'.EXT; Think\Think::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(
'Behavior\CheckLangBehavior'
),
'action_begin' => array(
'Home\Behaviors\LocateBehavior',
'Home\Behaviors\TokenBehavior',
'Home\Behaviors\CookieBehavior'
),
'view_begin' => array(
'Home\Behaviors\SeoBehavior',
'Home\Behaviors\ParseBehavior'
),
'view_end'=>array(
'Home\Behaviors\SlowLogBehavior',
),
);
例如上面这个配置文件使用了4个载点,分别在下面挂上了自己的behavior扩展。 这样thinkphp在执行到加载控制器方法action_begin时,就会先执行用户自己定义的三个扩展
‘Home\Behaviors\LocateBehavior’, ‘Home\Behaviors\TokenBehavior’, ‘Home\Behaviors\CookieBehavior’
下面让我们来详细看看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('Think\View');
//控制器初始化
if(method_exists($this,'_initialize'))
$this->_initialize();
}
ajaxReturn方法中监听了ajax_return。
Controller方法中我们可以看到thinkphp是如何在控制器中加入assign方法的。
$this->view = Think::instance('Think\View');
protected function assign($name,$value='') {
$this->view->assign($name,$value);
return $this;
}
同样的方法我们也可以在我们的behavior扩展中加入assign方法。