什么是鈎子
大家想必聽過插件,wordpress插件特別多,這個就是用鈎子機制實現的。
當代碼在運行的過程中,我們預先在運行的幾個特殊點里執行一些特殊方法:例如在運行方法(例如Blog::add的add方法)之前記錄輸入參數、運行方法之后記錄處理結果,這個運行方法之前、運行方法之后就是簡單的鈎子(掛載點),我們在這個鈎子上放置鈎子函數(記錄輸入參數、記錄處理結果),執行一些和程序運行不相關的任務。
<?php
class Blog extends Controller{
public function add(){
//some code
$res = $data;
return $res;
}
}
$obj = new Blog();
Log::write($_REQUEST);
$res = $obj->add();
Log::write(json_encode($res));
如果在運行方法之前放置的是一個OnBeforeRunActionCallback()的方法,這個方法可能最開始的時候是空的,但我們以后就可以不去修改原有代碼,直接在OnBeforeRunActionCallback()里面加代碼邏輯就可以了,例如記錄日志、參數過濾等等。
<?php
class Blog extends Controller{
public function add(){
//some code
$res = $data;
return $res;
}
}
$obj = new Blog();
OnBeforeRunActionCallback($_REQUEST);
$obj->add();
OnAfterRunActionCallback($res);
function OnBeforeRunActionCallback($param){
Log::write($param);
FilterParams($param);
}
function OnAfterRunActionCallback($res){
Log::write(json_encode($res));
}
在項目代碼中,你認為要擴展(暫時不擴展)的地方放置一個鈎子函數,等需要擴展的時候,把需要實現的類和函數掛載到這個鈎子上,就可以實現擴展了。
原理
實際的鈎子一般設計為一個類Hook,該類提供注冊插件到鈎子(add_hook)、觸發鈎子方法(trigger_hook)。注冊插件的時候將插件所要運行的可執行方法存儲到鈎子對應的數組里面。
$_listeners = array(
'OnBeforeRunAction' => array(
'callback1',
'callback2',
'callback3',
),
);
//提前注冊插件到鈎子
add_hook('OnBeforeRunAction', 'callback4');
//特定地方執行鈎子
trigger_hook('OnBeforeRunAction');
當觸發鈎子的時候,將遍歷OnBeforeRunAction里注冊的回調方法,執行對應的回調方法,實現動態擴展功能。注冊的鈎子方法一般是匿名函數:
function trigger_hook($hook, $data=''){
//查看要實現的鈎子,是否在監聽數組之中
if (isset($this->_listeners[$hook]) && is_array($this->_listeners[$hook]) && count($this->_listeners[$hook]) > 0)
{
// 循環調用開始
foreach ($this->_listeners[$hook] as $listener)
{
if(is_callable()){
call_user_func($listener, $data);
}elseif(is_array($listener)){
// 取出插件對象的引用和方法
$class =& $listener[0];
$method = $listener[1];
if(method_exists($class,$method))
{
// 動態調用插件的方法
$class->$method($data);
}
}
}
}
}
如何實現
簡單的
1、插件類Hook:提供注冊插件和執行插件的方法,實際是往一個數組里存掛載點對應的可執行方法。
2、在某個配置文件或者函數里統一注冊插件。
class Hook
{
//action hooks array
private static $actions = array();
/**
* ads a function to an action hook
* @param $hook
* @param $function
*/
public static function add_action($hook,$function)
{
$hook=mb_strtolower($hook,CHARSET);
// create an array of function handlers if it doesn't already exist
if(!self::exists_action($hook))
{
self::$actions[$hook] = array();
}
// append the current function to the list of function handlers
if (is_callable($function))
{
self::$actions[$hook][] = $function;
return TRUE;
}
return FALSE ;
}
/**
* executes the functions for the given hook
* @param string $hook
* @param array $params
* @return boolean true if a hook was setted
*/
public static function do_action($hook,$params=NULL)
{
$hook=mb_strtolower($hook,CHARSET);
if(isset(self::$actions[$hook]))
{
// call each function handler associated with this hook
foreach(self::$actions[$hook] as $function)
{
if (is_array($params))
{
call_user_func_array($function,$params);
}
else
{
call_user_func($function);
}
//cant return anything since we are in a loop! dude!
}
return TRUE;
}
return FALSE;
}
/**
* gets the functions for the given hook
* @param string $hook
* @return mixed
*/
public static function get_action($hook)
{
$hook=mb_strtolower($hook,CHARSET);
return (isset(self::$actions[$hook]))? self::$actions[$hook]:FALSE;
}
/**
* check exists the functions for the given hook
* @param string $hook
* @return boolean
*/
public static function exists_action($hook)
{
$hook=mb_strtolower($hook,CHARSET);
return (isset(self::$actions[$hook]))? TRUE:FALSE;
}
}
/**
* Hooks Shortcuts not in class
*/
function add_action($hook,$function)
{
return Hook::add_action($hook,$function);
}
function do_action($hook)
{
return Hook::do_action($hook);
}
用法舉例:
//添加鈎子
Hook::add_action('unique_name_hook','some_class::hook_test');
//或使用快捷函數添加鈎子:
add_action('unique_name_hook','other_class::hello');
add_action('unique_name_hook','some_public_function');
//執行鈎子
do_action('unique_name_hook');//也可以使用 Hook::do_action();
帶安裝/卸載的
鈎子類初始化的時候去注冊已經開啟的插件(如數據庫記錄);全局合適的時候設置掛載點;運行到合適的時候觸發掛載點注冊的事件。
1、插件類Hook:提供注冊插件和執行插件的方法,實際是往一個數組里存掛載點對應的可執行方法。
2、插件類增加一個初始化的方法,去數據查找已經安裝的插件,運行插件必須執行的注冊方法(reg),注冊插件方法注冊鈎子到掛載點。
3、固定把插件放在某個目錄,並安照一定規范寫配置文件。后台有插件列表頁面,遍歷指定目錄下的插件。當安裝的時候,將插件信息記錄到數據庫,卸載的時候刪除數據庫記錄信息。
<?php
/**
* @file plugin.php
* @brief 插件核心類
* @note 觀察者模式,注冊事件,觸發事件
*/
class plugin extends IInterceptorBase
{
//默認開啟的插件列表
private static $defaultList = array("_verification","_goodsCategoryWidget","_authorization","_userInfo","_initData");
//已經注冊監聽
private static $_listen = array();
//加載插件
public static function init()
{
$pluginDB = new IModel('plugin');
$pluginList = $pluginDB->query("is_open = 1","class_name","sort asc");
//加載默認插件
foreach(self::$defaultList as $val)
{
$pluginList[]= array('class_name' => $val);
}
foreach($pluginList as $key => $val)
{
$className = $val['class_name'];
$classFile = self::path().$className."/".$className.".php";
if(is_file($classFile))
{
include_once($classFile);
$pluginObj = new $className();
$pluginObj->reg();
}
}
}
/**
* @brief 注冊事件
* @param string $event 事件
* @param object ro function $classObj 類實例 或者 匿名函數
* @param string $method 方法名字
*/
public static function reg($event,$classObj,$method = "")
{
if(!isset(self::$_listen[$event]))
{
self::$_listen[$event] = array();
}
self::$_listen[$event][] = array($classObj,$method);
}
/**
* @brief 顯示已注冊事件
* @param string $event 事件名稱
* @return array
*/
public static function get($event = '')
{
if($event)
{
if( isset(self::$_listen[$event]) )
{
return self::$_listen[$event];
}
return null;
}
return self::$_listen;
}
/**
* @brief 觸發事件
* @param string $event 事件
* @param mixed $data 數據
* @notice 可以調用匿名函數和方法
*/
public static function trigger($event,$data = null)
{
$result = array();
if(isset(self::$_listen[$event]))
{
foreach(self::$_listen[$event] as $key => $val)
{
list($pluginObj,$pluginMethod) = $val;
$result[$key] = is_callable($pluginObj) ? call_user_func($pluginObj,$data):call_user_func(array($pluginObj,$pluginMethod),$data);
}
}
return isset($result[1]) ? $result : current($result);
}
/**
* @brief 插件物理路徑
* @return string 路徑字符串
*/
public static function path()
{
return IWeb::$app->getBasePath()."plugins/";
}
/**
* @brief 插件WEB路徑
* @return string 路徑字符串
*/
public static function webPath()
{
return IUrl::creatUrl('')."plugins/";
}
/**
* @brief 獲取全部插件
* @param string $name 插件名字,如果為空則獲取全部插件信息
* @return array 插件信息 array(
"name" => 插件名字,
"description" => 插件描述,
"explain" => 使用說明,
"class_name" => 插件ID,
"is_open" => 是否開啟,
"is_install" => 是否安裝,
"config_name" => 默認插件參數結構,
"config_param"=> 已經保存的插件參數,
"sort" => 排序,
)
*/
public static function getItems($name = '')
{
$result = array();
$dirRes = opendir(self::path());
//遍歷目錄讀取配置文件
$pluginDB = new IModel('plugin');
while($dir = readdir($dirRes))
{
if($dir[0] == "." || $dir[0] == "_")
{
continue;
}
if($name && $result)
{
break;
}
if($name && $dir != $name)
{
continue;
}
$pluginIndex = self::path().$dir."/".$dir.".php";
if(is_file($pluginIndex))
{
include_once($pluginIndex);
if(get_parent_class($dir) == "pluginBase")
{
$class_name = $dir;
$pluginRow = $pluginDB->getObj('class_name = "'.$class_name.'"');
$is_open = $pluginRow ? $pluginRow['is_open'] : 0;
$is_install = $pluginRow ? 1 : 0;
$sort = $pluginRow ? $pluginRow['sort'] : 99;
$config_param = array();
if($pluginRow && $pluginRow['config_param'])
{
$config_param = JSON::decode($pluginRow['config_param']);
}
$result[$dir] = array(
"name" => $class_name::name(),
"description" => $class_name::description(),
"explain" => $class_name::explain(),
"class_name" => $class_name,
"is_open" => $is_open,
"is_install" => $is_install,
"config_name" => $class_name::configName(),
"config_param"=> $config_param,
"sort" => $sort,
);
}
}
}
if(!$name)
{
return $result;
}
return isset($result[$name]) ? $result[$name] : array();
}
/**
* @brief 系統內置的所有事件觸發
*/
public static function onCreateApp(){plugin::init();plugin::trigger("onCreateApp");}
public static function onFinishApp(){plugin::trigger("onFinishApp");}
public static function onBeforeCreateController($ctrlId){plugin::trigger("onBeforeCreateController",$ctrlId);plugin::trigger("onBeforeCreateController@".$ctrlId);}
public static function onCreateController($ctrlObj){plugin::trigger("onCreateController");plugin::trigger("onCreateController@".$ctrlObj->getId());}
public static function onFinishController($ctrlObj){plugin::trigger("onFinishController");plugin::trigger("onFinishController@".$ctrlObj->getId());}
public static function onBeforeCreateAction($ctrlObj,$actionId){plugin::trigger("onBeforeCreateAction",$actionId);plugin::trigger("onBeforeCreateAction@".$ctrlObj->getId());plugin::trigger("onBeforeCreateAction@".$ctrlObj->getId()."@".$actionId);}
public static function onCreateAction($ctrlObj,$actionObj){plugin::trigger("onCreateAction");plugin::trigger("onCreateAction@".$ctrlObj->getId());plugin::trigger("onCreateAction@".$ctrlObj->getId()."@".$actionObj->getId());}
public static function onFinishAction($ctrlObj,$actionObj){plugin::trigger("onFinishAction");plugin::trigger("onFinishAction@".$ctrlObj->getId());plugin::trigger("onFinishAction@".$ctrlObj->getId()."@".$actionObj->getId());}
public static function onCreateView($ctrlObj,$actionObj){plugin::trigger("onCreateView");plugin::trigger("onCreateView@".$ctrlObj->getId());plugin::trigger("onCreateView@".$ctrlObj->getId()."@".$actionObj->getId());}
public static function onFinishView($ctrlObj,$actionObj){plugin::trigger("onFinishView");plugin::trigger("onFinishView@".$ctrlObj->getId());plugin::trigger("onFinishView@".$ctrlObj->getId()."@".$actionObj->getId());}
public static function onPhpShutDown(){plugin::trigger("onPhpShutDown");}
}
/**
* @brief 插件基類,所有插件必須繼承此類
* @notice 必須實現3個抽象方法: reg(),name(),description()
*/
abstract class pluginBase extends IInterceptorBase
{
//錯誤信息
protected $error = array();
//注冊事件接口,內部通過調用payment::reg(事件,對象實例,方法);
public function reg(){}
/**
* @brief 默認插件參數信息,寫入到plugin表config_param字段
* @return array("字段名" => array(
"name" => "文字顯示",
"type" => "數據類型【text,radio,checkbox,select】",
"pattern" => "數據校驗【int,float,date,datetime,require,正則表達式】",
"value" => "1,數組:枚舉數據【radio,checkbox,select】的預設值,array(名字=>數據); 2,字符串:【text】默認數據",
))
*/
public static function configName()
{
return array();
}
/**
* @brief 插件安裝
* @return boolean
*/
public static function install()
{
return true;
}
/**
* @brief 插件卸載
* @return boolean
*/
public static function uninstall()
{
return true;
}
/**
* @brief 插件名字
* @return string
*/
public static function name()
{
return "插件名稱";
}
/**
* @brief 插件功能描述
* @return string
*/
public static function description()
{
return "插件描述";
}
/**
* @brief 插件使用說明
* @return string
*/
public static function explain()
{
return "";
}
/**
* @brief 獲取DB中錄入的配置參數
* @return array
*/
public function config()
{
$className= get_class($this);
$pluginDB = new IModel('plugin');
$dataRow = $pluginDB->getObj('class_name = "'.$className.'"');
if($dataRow && $dataRow['config_param'])
{
return JSON::decode($dataRow['config_param']);
}
return array();
}
/**
* @brief 返回錯誤信息
* @return array
*/
public function getError()
{
return $this->error ? join("\r\n",$this->error) : "";
}
/**
* @brief 寫入錯誤信息
* @return array
*/
public function setError($error)
{
$this->error[] = $error;
}
/**
* @brief 插件視圖渲染有布局
* @param string $view 視圖名字
* @param array $data 視圖里面的數據
*/
public function redirect($view,$data = array())
{
if($data === true)
{
$this->controller()->redirect($view);
}
else
{
$__className = get_class($this);
$__pluginViewPath = plugin::path().$__className."/".$view;
$result = self::controller()->render($__pluginViewPath,$data);
if($result === false)
{
IError::show($__className."/".$view."插件視圖不存在");
}
}
}
/**
* @brief 插件視圖渲染去掉布局
* @param string $view 視圖名字
* @param array $data 視圖里面的數據
*/
public function view($view,$data = array())
{
self::controller()->layout = "";
$this->redirect($view,$data);
}
/**
* @brief 插件物理目錄
* @param string 插件路徑地址
*/
public function path()
{
return plugin::path().get_class($this)."/";
}
/**
* @brief 插件WEB目錄
* @param string 插件路徑地址
*/
public function webPath()
{
return plugin::webPath().get_class($this)."/";
}
}
哪些系統存在
1、wordpress
2、Discuz:Discuz! 插件制作教程_Discuz! 資料庫
3、ThinkPHP
4、OneThink 什么是鈎子? - OneThink1.0開發手冊
參考資料
1、php中的鈎子(hook插件機制) - MasonZhang - 博客園
http://www.cnblogs.com/miketwais/articles/hook.html
2、PHP鈎子系統 - ThinkPHP框架
http://www.thinkphp.cn/code/337.html
3、PHP中的插件機制原理和實例_php實例_腳本之家
http://www.jb51.net/article/51980.htm
