理解什麼是鉤子
理解鉤子Hook以及在Thinkphp下利用鉤子使用行爲擴展什麼是鉤子函數
個人理解:鉤子就像一個”陷阱”、”監聽器”,當A發送一個消息到B時,當消息還未到達目的地B時,被鉤子攔截調出一部分代碼做處理,這部分代碼也叫鉤子函數或者回調函數
參考網上說法
譬如我們用鼠標在某個窗口上雙擊了一次, 或者給某個窗口輸入了一個字母 A;
首先發現這些事件的不是窗口, 而是系統!然後系統告訴窗口: 喂! 你讓人點了, 並且是連續點了兩鼠標, 你準備怎麼辦?
或者是系統告訴窗口: 喂! 有人向你家裏扔磚頭了, 不信你看看, 那塊磚頭是 A.這時窗口的對有些事件會忽略、對有些事件會做出反應:
譬如, 可能對鼠標單擊事件忽略, 窗口想: 你單擊我不要緊, 累死你我不負責;
但一旦誰要雙擊我, 我會馬上行動, 給你點顏色瞧瞧!
這裏窗口準備要採取的行動, 就是我們提前寫好的事件.
用 Windows 的話說, 窗口的事件就是系統發送給窗口的消息; 窗口要採取的行動(事件代碼) 就是窗口>的回調函數.但是! 往往隔牆有耳. 系統要通知給窗口的”話”(消息), 可能會被另一個傢伙(譬如是一個賊)提前聽>到!
有可能這個賊就是專門在這等情報的, 賊知道後, 往往在窗口知道以前就採取了行動!
並且這個賊對不同的消息會採取不同的行動方案, 它的行動方案一般也是早就準備好的;
當然這個賊也不是對什麼消息都感興趣, 對不感興趣的消息也就無須制定相應的行動方案.總結: 這個”賊”就是我們要設置的鉤子; “賊”的”行動方案”就是鉤子函數, 或者叫鉤子的回調函數.
上面一段話原文鏈接 http://www.cnblogs.com/del/archive/2008/02/25/1080825.html
總的來說,鉤子就像一個”掛載點”掛到函數上,當函數執行過程中遇到這個”掛載點”,掛載點(鉤子)就會將一塊代碼拉出來。比如,我們向腳本中傳入 了幾個參數,然後進行插入操作,插入之前我們有一個鉤子(before_insert)正在listen,當函數執行到這個鉤子時,就會去通過鉤子去調裏 面的一塊代碼,我們在這部分鉤子函數裏面可以對數據進行驗證或者加工等,達到你想要的操作
爲什麼使用鉤子
既然鉤子是一個監聽器,通過鉤子來調用一部分代碼做處理,那我直接在腳本里面另寫一個函數A然後在函數B執行過程中去調用A不就好了?
個人認爲:鉤子函數相對於直接在函數中調用另外一個函數來說,更加安全方便。當我們需要修改擴展功能時,我們無需修改函數B中的鉤子,只需要修 改鉤子裏面的代碼塊即可,而如果直接修改函數A,則會對函數B所在類進行頻繁修改。違背了封閉原則。另一點,利用鉤子對後期的維護和功能擴展更加方便。
Thinkphp中使用鉤子(行爲擴展)
在TP中利用鉤子,其實就是行爲擴展
行爲
行爲(Behavior)是一個比較抽象的概念,你可以想象成在應用執行過程中的一個動作或者處理,在框架 的執行流程中,各個位置都可以有行爲產生,例如路由檢測是一個行爲,靜態緩存是一個行爲,用戶權限檢測也是行爲,大到業務邏輯,小到瀏覽器檢測、多語言檢 測等等都可以當做是一個行爲,甚至說你希望給你的網站用戶的第一次訪問彈出Hello,world!這些都可以看成是一種行爲,行爲的存在讓你無需改動框架和應用,而在外圍通過擴展或者配置來改變或者增加一些功能。
而不同的行爲之間也具有位置共同性,比如,有些行爲的作用位置都是在應用執行前,有些行爲都是在模板輸出之後,我們把這些行爲發生作用的位置稱之爲標籤(位)(tag),當應用程序運行到這個標籤的時候,就會被攔截下來,統一執行相關的行爲
這裏的行爲相當於鉤子函數,想要”使用”它,我們就必須藉助鉤子Hook
添加行爲擴展
Thinkphp中有系統的行爲擴展,在Lib\Think\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_init’),Hook::listen(‘app_begin’); 相當於鉤子監聽的這些tags(這幾個tags是系統內置的行爲,無需再註冊)。要觸發自定義行爲,必須先註冊行爲,ThinkPHP中提供了自動註冊和手動註冊 。
自動註冊
TP裏面Lib\Think目錄下面Think.class.php中有這兩行代碼
// 加載模式行爲定義 if(isset($mode['tags'])) { Hook::import(is_array($mode['tags'])?$mode['tags']:include $mode['tags']); } // 加載應用行爲定義 if(is_file(CONF_PATH.'tags.php'))// 允許應用增加開發模式配置定義 Hook::import(include CONF_PATH.'tags.php');
模式行爲是系統內置的,我們可以在Lib\Mode\common.php找到
// 行爲擴展定義 'tags' => array( 'app_init' => array('Behavior\BuildLiteBehavior', // 生成運行Lite文件 ), 'app_begin' => array( 'Behavior\ReadHtmlCacheBehavior', // 讀取靜態緩存 ), 'app_end' => array( 'Behavior\ShowPageTraceBehavior', // 頁面Trace顯示 ), 'view_parse' => array( 'Behavior\ParseTemplateBehavior', // 模板解析 支持PHP、內置模板引擎和第三方模板引擎 ), 'template_filter' => array( 'Behavior\ContentReplaceBehavior', // 模板輸出替換 ), 'view_filter' => array 'Behavior\WriteHtmlCacheBehavior', // 寫入靜態緩存 ), ),
用戶自定義的行爲擴展,需要在CONF_PATH.’tags.php’也就是/Common/conf/tags.php自行創建,舉例
<?php return array( "action_begin" => array('Home\\Behaviors\\testBehavior') //一個標籤可以有多個行爲,我們也可以這樣 "action_begin" => array('Home\Behaviors\test1Behavior','Home\\Behaviors\\test2Behavior') ); ?>
TP在運行時爲自動加載這些行爲。我們只需寫好自己的行爲擴展,然後在某個地方監聽(Hook::listen(tags)),就可以觸發這些行爲了
手動註冊
說到這,我們還是先看下Lib\Think\Hook.class.php的代碼
/** * 動態添加插件到某個標籤 * @param string $tag 標籤名稱 * @param mixed $name 插件名稱 * @return void */ 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; } }
代碼十分簡單,我們註冊行爲只需要Hook::add(tags,name)
注意:這裏面name必須是一個包含命名空間路徑的類,比如Home\Behavior\testBehavior ,類名後面必須是Behavior結尾,類中必須實現run方法。原因:請看Hook類中代碼
/** * 執行某個插件 * @param string $name 插件名稱 * @param string $tag 方法名 * @param Mixed $params 傳入的參數 */ static public function exec($name, $tag,&$params=NULL) { //這裏截取後八位類名字符串,所以必須是Behavior if('Behavior' == substr($name,-8) ){ // if('testBehavior' == substr($name,-12) // { // $tag = 'test'; // } // 行爲擴展必須用run入口方法 $tag = 'run'; } echo $name.'<br/>'; $addon = new $name(); return $addon->$tag($params); }
當然,你也可以修改規則,比如我不想以Behavior爲類名結尾,也不想調用run方法,這時候想修改只能在 listen()方法裏面進行判斷修改,如果直接在exec()裏面修改,立馬報錯,因爲系統的內置行爲都是按這個規則來的啊!除非你把源碼中的行爲類名 和行爲方法run改掉,當然我相信你不會這麼傻
其他事項:
觸發行爲的關鍵方法是Hook類中的listen方法,它通過遍歷某個行爲標籤下的所有行爲,依次實例化並調用run方法
listen方法中,如果之前在配置文件中開啓了DEBUG模式,則它會生成日誌記錄你的行爲,這裏面牽涉到很多的IO操作,所以你的項目完成時建議取消DEBUG模式以提升速度
listen方法中,允許傳遞參數且只允許傳遞一個參數(傳多個可以用數組呢),不過這個參數是引用傳值,所以只能傳入變量,傳入常量會報錯
最後,Lib\Think\Behavior.class.php,這個抽象類中只有一個抽象方法run(),在你的自己行爲擴展中建議繼承它,儘管這不是必須的,但是這樣更加規範。。。不過TP內置的系統行爲都沒繼承這個抽象類,也不知道鬧哪樣
流程與舉例
使用鉤子觸發行爲擴展的流程:
自動註冊(Common/Conf/tags.php按格式自己添加),或者 手動註冊(類中方法如初始方法,調用Hook::add(tags,name));
寫好自己的行爲類,類名以Behavior結尾,實現run方法
在需要添加行爲的函數裏 ,直接Hook::Listen(tags,prarm),注意一定要傳變量,不需要傳常量。
這樣,整個過程就結束了,下面舉個例子
舉例
鄙人最近寫了個人網站(小博客) http://www.huangtianer.com/
我需要記錄一下網站的PV,於是我在BaseController裏面的初始化方法
class BaseController extends Controller{ public function _initialize() { //記錄訪問 //這裏我是手動註冊的行爲 Hook::add('mark_pv','Home\Behaviors\testBehavior'); hook::listen('mark_pv'); } }
然後我再Home\Behaviors\testBehavior.class.php中
class testBehavior extends Behavior { public function run(&$param) { //記錄數據 $data['client_ip'] = get_client_ip(); $data['page_view'] = CONTROLLER_NAME.'/'.ACTION_NAME; $data['create_time'] = time(); $data['status'] = 0; M('page_view')->add($data); } }
運行後正常插入數據,而且如果後期我的個人網站數據庫由於訪問量太大(呵呵)頂不住壓力,不能再記錄PV了,我直接刪除鉤子函數裏面的代碼,不需要動鉤子,就解決了問題,比較方便。
結束語:如有BUG或者建議,歡迎指正