PHP钩子机制

@date:2016-08-29 22:42:00

什么是钩子 #

大家想必听过插件,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

Build by Loppo 0.6.14