Home >Backend Development >PHP Tutorial >懂得钩子Hook以及在Thinkphp下利用钩子使用行为扩展

懂得钩子Hook以及在Thinkphp下利用钩子使用行为扩展

WBOY
WBOYOriginal
2016-06-13 12:23:15923browse

理解钩子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中,有这个函数

<code class=" hljs java">  <span class="hljs-javadoc">/**     * 运行应用实例 入口文件使用的快捷方法     *<span class="hljs-javadoctag"> @access</span> public     *<span class="hljs-javadoctag"> @return</span> void     */</span>    <span class="hljs-keyword">static</span> <span class="hljs-keyword">public</span> function <span class="hljs-title">run</span>() {        <span class="hljs-comment">// 应用初始化标签</span>        Hook::listen(<span class="hljs-string">'app_init'</span>);        App::init();        <span class="hljs-comment">// 应用开始标签</span>        Hook::listen(<span class="hljs-string">'app_begin'</span>);        <span class="hljs-comment">// Session初始化</span>        <span class="hljs-keyword">if</span>(!IS_CLI){            session(C(<span class="hljs-string">'SESSION_OPTIONS'</span>));        }        <span class="hljs-comment">// 记录应用初始化时间</span>        G(<span class="hljs-string">'initTime'</span>);        App::exec();        <span class="hljs-comment">// 应用结束标签</span>        Hook::listen(<span class="hljs-string">'app_end'</span>);        <span class="hljs-keyword">return</span> ;    }</code>

其中里面的Hook::listen(‘app_init’),Hook::listen(‘app_begin’); 相当于钩子监听的这些tags(这几个tags是系统内置的行为,无需再注册)。要触发自定义行为,必须先注册行为,ThinkPHP中提供了自动注册和手动注册 。

自动注册

TP里面Lib\Think目录下面Think.class.php中有这两行代码

<code class=" hljs php">         <span class="hljs-comment">// 加载模式行为定义</span>          <span class="hljs-keyword">if</span>(<span class="hljs-keyword">isset</span>(<span class="hljs-variable">$mode</span>[<span class="hljs-string">'tags'</span>])) {              Hook::import(is_array(<span class="hljs-variable">$mode</span>[<span class="hljs-string">'tags'</span>])?<span class="hljs-variable">$mode</span>[<span class="hljs-string">'tags'</span>]:<span class="hljs-keyword">include</span> <span class="hljs-variable">$mode</span>[<span class="hljs-string">'tags'</span>]);          }          <span class="hljs-comment">// 加载应用行为定义</span>          <span class="hljs-keyword">if</span>(is_file(CONF_PATH.<span class="hljs-string">'tags.php'</span>))              <span class="hljs-comment">// 允许应用增加开发模式配置定义</span>              Hook::import(<span class="hljs-keyword">include</span> CONF_PATH.<span class="hljs-string">'tags.php'</span>);   </code>

模式行为是系统内置的,我们可以在Lib\Mode\common.php找到

<code class=" hljs php"> <span class="hljs-comment">// 行为扩展定义</span>    <span class="hljs-string">'tags'</span>  =>  <span class="hljs-keyword">array</span>(        <span class="hljs-string">'app_init'</span>     =>  <span class="hljs-keyword">array</span>(            <span class="hljs-string">'Behavior\BuildLiteBehavior'</span>, <span class="hljs-comment">// 生成运行Lite文件</span>        ),                <span class="hljs-string">'app_begin'</span>     =>  <span class="hljs-keyword">array</span>(            <span class="hljs-string">'Behavior\ReadHtmlCacheBehavior'</span>, <span class="hljs-comment">// 读取静态缓存</span>        ),        <span class="hljs-string">'app_end'</span>       =>  <span class="hljs-keyword">array</span>(            <span class="hljs-string">'Behavior\ShowPageTraceBehavior'</span>, <span class="hljs-comment">// 页面Trace显示</span>        ),        <span class="hljs-string">'view_parse'</span>    =>  <span class="hljs-keyword">array</span>(            <span class="hljs-string">'Behavior\ParseTemplateBehavior'</span>, <span class="hljs-comment">// 模板解析 支持PHP、内置模板引擎和第三方模板引擎</span>        ),        <span class="hljs-string">'template_filter'</span>=> <span class="hljs-keyword">array</span>(            <span class="hljs-string">'Behavior\ContentReplaceBehavior'</span>, <span class="hljs-comment">// 模板输出替换</span>        ),        <span class="hljs-string">'view_filter'</span>   =>  <span class="hljs-keyword">array</span>(            <span class="hljs-string">'Behavior\WriteHtmlCacheBehavior'</span>, <span class="hljs-comment">// 写入静态缓存</span>        ),    ),</code>

用户自定义的行为扩展,需要在CONF_PATH.’tags.php’也就是/Common/conf/tags.php自行创建,举例

<code class=" hljs xml"><span class="php"><span class="hljs-preprocessor"><?php</span> <span class="hljs-keyword">return</span> <span class="hljs-keyword">array</span>(    <span class="hljs-string">"action_begin"</span> => <span class="hljs-keyword">array</span>(<span class="hljs-string">'Home\\Behaviors\\testBehavior'</span>)    <span class="hljs-comment">//一个标签可以有多个行为,我们也可以这样</span>    <span class="hljs-string">"action_begin"</span> => <span class="hljs-keyword">array</span>(<span class="hljs-string">'Home\Behaviors\test1Behavior'</span>,<span class="hljs-string">'Home\\Behaviors\\test2Behavior'</span>)); <span class="hljs-preprocessor">?></span></span></code>

TP在运行时为自动加载这些行为。我们只需写好自己的行为扩展,然后在某个地方监听(Hook::listen(tags)),就可以触发这些行为了

手动注册

说到这,我们还是先看下Lib\Think\Hook.class.php的代码

<code class=" hljs php">    <span class="hljs-comment">/**     * 动态添加插件到某个标签     *<span class="hljs-phpdoc"> @param</span> string $tag 标签名称     *<span class="hljs-phpdoc"> @param</span> mixed $name 插件名称     *<span class="hljs-phpdoc"> @return</span> void     */</span>    <span class="hljs-keyword">static</span> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">add</span><span class="hljs-params">(<span class="hljs-variable">$tag</span>,<span class="hljs-variable">$name</span>)</span> {</span>        <span class="hljs-keyword">if</span>(!<span class="hljs-keyword">isset</span>(<span class="hljs-keyword">self</span>::<span class="hljs-variable">$tags</span>[<span class="hljs-variable">$tag</span>])){            <span class="hljs-keyword">self</span>::<span class="hljs-variable">$tags</span>[<span class="hljs-variable">$tag</span>]   =   <span class="hljs-keyword">array</span>();        }        <span class="hljs-keyword">if</span>(is_array(<span class="hljs-variable">$name</span>)){            <span class="hljs-keyword">self</span>::<span class="hljs-variable">$tags</span>[<span class="hljs-variable">$tag</span>]   =   array_merge(<span class="hljs-keyword">self</span>::<span class="hljs-variable">$tags</span>[<span class="hljs-variable">$tag</span>],<span class="hljs-variable">$name</span>);        }<span class="hljs-keyword">else</span>{            <span class="hljs-keyword">self</span>::<span class="hljs-variable">$tags</span>[<span class="hljs-variable">$tag</span>][] =   <span class="hljs-variable">$name</span>;        }    }</code>

代码十分简单,我们注册行为只需要Hook::add(tags,name)

注意:这里面name必须是一个包含命名空间路径的类,比如Home\Behavior\testBehavior ,类名后面必须是Behavior结尾,类中必须实现run方法。原因:请看Hook类中代码

<code class=" hljs php">  <span class="hljs-comment">/**     * 执行某个插件     *<span class="hljs-phpdoc"> @param</span> string $name 插件名称     *<span class="hljs-phpdoc"> @param</span> string $tag 方法名      *<span class="hljs-phpdoc"> @param</span> Mixed $params 传入的参数     */</span>    <span class="hljs-keyword">static</span> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">exec</span><span class="hljs-params">(<span class="hljs-variable">$name</span>, <span class="hljs-variable">$tag</span>,&<span class="hljs-variable">$params</span>=NULL)</span> {</span>        <span class="hljs-comment">//这里截取后八位类名字符串,所以必须是Behavior</span>        <span class="hljs-keyword">if</span>(<span class="hljs-string">'Behavior'</span> == substr(<span class="hljs-variable">$name</span>,-<span class="hljs-number">8</span>) ){             <span class="hljs-comment">// if('testBehavior' == substr($name,-12)</span>            <span class="hljs-comment">// {</span>            <span class="hljs-comment">//     $tag = 'test';</span>            <span class="hljs-comment">// }</span>            <span class="hljs-comment">// 行为扩展必须用run入口方法</span>               <span class="hljs-variable">$tag</span>    =   <span class="hljs-string">'run'</span>;        }        <span class="hljs-keyword">echo</span> <span class="hljs-variable">$name</span>.<span class="hljs-string">'<br/>'</span>;        <span class="hljs-variable">$addon</span>   = <span class="hljs-keyword">new</span> <span class="hljs-variable">$name</span>();        <span class="hljs-keyword">return</span> <span class="hljs-variable">$addon</span>-><span class="hljs-variable">$tag</span>(<span class="hljs-variable">$params</span>);    }</code>

当然,你也可以修改规则,比如我不想以Behavior为类名结尾,也不想调用run方法,这时候想修改只能在listen()方法里面进行判断修改,如果直接在exec()里面修改,立马报错,因为系统的内置行为都是按这个规则来的啊!除非你把源码中的行为类名和行为方法run改掉,当然我相信你不会这么傻

其他事项:

  • 触发行为的关键方法是Hook类中的listen方法,它通过遍历某个行为标签下的所有行为,依次实例化并调用run方法
  • listen方法中,如果之前在配置文件中开启了DEBUG模式,则它会生成日志记录你的行为,这里面牵涉到很多的IO操作,所以你的项目完成时建议取消DEBUG模式以提升速度
  • listen方法中,允许传递参数且只允许传递一个参数(传多个可以用数组呢),不过这个参数是引用传值,所以只能传入变量,传入常量会报错
  • 最后,Lib\Think\Behavior.class.php,这个抽象类中只有一个抽象方法run(),在你的自己行为扩展中建议继承它,尽管这不是必须的,但是这样更加规范。。。不过TP内置的系统行为都没继承这个抽象类,也不知道闹哪样

流程与举例

使用钩子触发行为扩展的流程:

  1. 自动注册(Common/Conf/tags.php按格式自己添加),或者 手动注册(类中方法如初始方法,调用Hook::add(tags,name));
  2. 写好自己的行为类,类名以Behavior结尾,实现run方法
  3. 在需要添加行为的函数里 ,直接Hook::Listen(tags,prarm),注意一定要传变量,不需要传常量。

这样,整个过程就结束了,下面举个例子

举例

鄙人最近写了个人网站(小博客) http://www.huangtianer.com/
我需要记录一下网站的PV,于是我在BaseController里面的初始化方法

<code class="language-php hljs "><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BaseController</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span>{</span>     <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_initialize</span><span class="hljs-params">()</span>     {</span>         <span class="hljs-comment">//记录访问</span>         <span class="hljs-comment">//这里我是手动注册的行为</span>         Hook::add(<span class="hljs-string">'mark_pv'</span>,<span class="hljs-string">'Home\Behaviors\testBehavior'</span>);         hook::listen(<span class="hljs-string">'mark_pv'</span>);        }}</code>

然后我再Home\Behaviors\testBehavior.class.php中

<code class=" hljs php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">testBehavior</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Behavior</span> {</span>    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">run</span><span class="hljs-params">(&<span class="hljs-variable">$param</span>)</span>    {</span>        <span class="hljs-comment">//记录数据</span>        <span class="hljs-variable">$data</span>[<span class="hljs-string">'client_ip'</span>] = get_client_ip();        <span class="hljs-variable">$data</span>[<span class="hljs-string">'page_view'</span>] = CONTROLLER_NAME.<span class="hljs-string">'/'</span>.ACTION_NAME;        <span class="hljs-variable">$data</span>[<span class="hljs-string">'create_time'</span>] = time();        <span class="hljs-variable">$data</span>[<span class="hljs-string">'status'</span>] = <span class="hljs-number">0</span>;        M(<span class="hljs-string">'page_view'</span>)->add(<span class="hljs-variable">$data</span>);    }}</code>

运行后正常插入数据,而且如果后期我的个人网站数据库由于访问量太大(呵呵)顶不住压力,不能再记录PV了,我直接删除钩子函数里面的代码,不需要动钩子,就解决了问题,比较方便。

结束语:如有BUG或者建议,欢迎指正

版权声明:本文为博主原创文章,未经博主允许不得转载。

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn