CodeIgniter Hooks 钩子机制完全指南:在不改动核心文件的前提下扩展框架执行流程
【免费下载链接】CodeIgniterOpen Source PHP Framework (originally from EllisLab)项目地址: https://gitcode.com/gh_mirrors/co/CodeIgniter
Hooks(钩子)是 CodeIgniter 框架提供的一套"事件回调"机制:它允许你在框架固定执行流程的特定节点插入自己的类方法或函数,从而在不修改system/核心文件的前提下扩展框架行为。本文以 CodeIgniter 3.x 的 Hooks 功能为核心,完整讲解钩子的启用方式、七种标准 Hook 点、配置写法与源码级执行原理,读完你可以在自己的应用中熟练定义单个、多个乃至匿名函数形式的钩子,并理解pre_system到post_system每个节点的确切触发时机。
一、Hooks 是什么:为"不可改动"的核心流程开一扇窗
CodeIgniter 从入口文件index.php开始,会按照固定顺序完成"初始化基准测试与钩子类 → 路由解析 → 安全检查 → 加载控制器 → 渲染视图 → 输出响应"这一整套执行流程(完整流程见 Application Flow)。官方文档明确指出:"CodeIgniter's Hooks feature provides a means to tap into and modify the inner workings of the framework without hacking the core files."——这正是 Hooks 的设计初衷。
与直接修改system/core/下源码(升级框架时会丢失改动)相比,Hooks 把"定制点"全部收敛到应用目录内:
- 钩子定义文件位于
application/config/hooks.php,属于你的应用而非框架核心; - 钩子脚本存放在
application/目录内部的任意子目录(通常为application/hooks/); - 框架核心的 CodeIgniter.php 中只负责在特定阶段调用
$EXT->call_hook('xxx'),具体的钩子脚本完全由你的应用决定。
典型应用场景包括:在控制器加载前统一做权限校验、在控制器实例化后注入公共数据、在页面输出前对最终 HTML 做后处理、用自定义机制接管缓存读取与页面展示等。
二、启用 Hooks:一个配置项开关
Hooks 功能默认是关闭的。要全局启用它,在 application/config/config.php 中把enable_hooks改为TRUE:
$config['enable_hooks'] = TRUE;该配置项在仓库中的默认值为FALSE(见 application/config/config.php)。
从源码看这个开关的作用非常直接:CI_Hooks类的构造函数会先检查该项,未启用时直接返回,不加载任何钩子定义(见 system/core/Hooks.php):
public function __construct(CI_Config $config) { log_message('info', 'Hooks Class Initialized'); // If hooks are not enabled in the config file // there is nothing else to do if ($config->item('enable_hooks') === FALSE) { return; } // ... }也就是说,只有enable_hooks为TRUE时,hooks.php才会被加载、$hook数组才会生效。
三、定义 Hook:数组原型与五个字段
钩子定义在application/config/hooks.php文件中。每个钩子是一个关联数组,原型如下:
$hook['pre_controller'] = array( 'class' => 'MyClass', 'function' => 'Myfunction', 'filename' => 'Myclass.php', 'filepath' => 'hooks', 'params' => array('beer', 'wine', 'snacks') );数组下标($hook['pre_controller'])对应你希望挂载的 Hook 点名称,下面五个字段逐项说明:
| 字段 | 是否必填 | 说明 |
|---|---|---|
class | 可选 | 要调用的类名。如果你更想用一个过程式函数而非类方法,将此留空即可(但此时function必须提供) |
function | 必填 | 要调用的函数名(或类方法名) |
filename | 必填 | 包含该类/函数的脚本文件名 |
filepath | 必填 | 存放脚本的目录名,相对application/目录,结尾不要带斜杠 |
params | 可选 | 传给脚本的参数,可以是任意值(数组、标量等) |
关于filepath的定位规则,文档强调:"Your script must be located in a directory INSIDE your application/ directory"——脚本必须位于application/内部。例如:
- 脚本位于
application/hooks/→filepath填'hooks'; - 脚本位于
application/hooks/utilities/→filepath填'hooks/utilities'; - 不要写尾部斜杠。
结合源码可以确认路径的真实拼接逻辑。在 system/core/Hooks.php 的_run_hook()方法中:
if ( ! isset($data['filepath'], $data['filename'])) { return FALSE; } $filepath = APPPATH.$data['filepath'].'/'.$data['filename']; if ( ! file_exists($filepath)) { return FALSE; }即最终文件路径 =APPPATH+filepath+/+filename,其中APPPATH指向你的application/目录。若文件不存在,钩子会静默失败(返回FALSE),不会抛出异常。
3.1 参数如何传递
params字段中定义的内容会作为单个参数传给钩子的函数/方法。继续看_run_hook()的实现(system/core/Hooks.php):
$class = empty($data['class']) ? FALSE : $data['class']; $function = empty($data['function']) ? FALSE : $data['function']; $params = isset($data['params']) ? $data['params'] : '';随后调用时无论是类方法还是普通函数,都统一以$function($params)的形式传入(见 system/core/Hooks.php 与 system/core/Hooks.php)。因此上面的例子中,Myfunction会收到array('beer', 'wine', 'snacks')作为它的第一个参数:
class MyClass { public function Myfunction($params) { // $params === array('beer', 'wine', 'snacks') } }3.2 类钩子与过程式函数钩子
- 类形式:
class非空时,框架会require_once该文件、实例化类并把实例缓存在$_objects属性中(同一类多次触发只实例化一次,见 system/core/Hooks.php),然后调用对应方法; - 函数形式:
class留空时,框架require_once文件后直接调用全局函数(见 system/core/Hooks.php)。
四、用匿名函数(闭包)定义 Hook
如果你希望钩子逻辑很短、不必单独建立文件,可以直接用 PHP 闭包定义钩子,语法更简洁:
$hook['post_controller'] = function() { /* do something here */ };从源码看,_run_hook()第一步就是检测可调用对象并直接执行(system/core/Hooks.php):
// Closures/lambda functions and array($object, 'method') callables if (is_callable($data)) { is_array($data) ? $data[0]->{$data[1]}() : $data(); return TRUE; }所以除了闭包,形如array($object, 'method')的可调用数组同样会被识别并直接执行——这为钩子提供了更灵活的写法。另外注意,闭包形式不会经过filepath/filename检查,因此无需也无法指定文件位置。
五、同一 Hook 点的多次调用
一个 Hook 点可以挂载多个脚本:只需把数组声明改为多维,并在下标后加[]:
$hook['pre_controller'][] = array( 'class' => 'MyClass', 'function' => 'MyMethod', 'filename' => 'Myclass.php', 'filepath' => 'hooks', 'params' => array('beer', 'wine', 'snacks') ); $hook['pre_controller'][] = array( 'class' => 'MyOtherClass', 'function' => 'MyOtherMethod', 'filename' => 'Myotherclass.php', 'filepath' => 'hooks', 'params' => array('red', 'yellow', 'blue') );注意每个下标后的方括号:$hook['pre_controller'][]。它让同一 Hook 点可以绑定多个脚本,定义顺序就是执行顺序——先定义的先执行。
源码中的判断逻辑与此完全对应(system/core/Hooks.php):
public function call_hook($which = '') { if ( ! $this->enabled OR ! isset($this->hooks[$which])) { return FALSE; } if (is_array($this->hooks[$which]) && ! isset($this->hooks[$which]['function'])) { foreach ($this->hooks[$which] as $val) { $this->_run_hook($val); } } else { $this->_run_hook($this->hooks[$which]); } return TRUE; }这里有个值得注意的细节:框架通过"该节点的值是否含有function键"来区分单钩子与多钩子。单个数组形式(含function键)直接执行;多维数组形式(不含function键)则逐个遍历执行,且严格按照你在hooks.php中的书写顺序。
六、七个标准 Hook 点及其精确触发时机
CodeIgniter 共提供七个标准 Hook 点,下表汇总了每个点的触发时机与典型用途:
| Hook 点 | 触发时机 | 此时已完成的工作 | 典型用途 |
|---|---|---|---|
pre_system | 系统执行的最早期 | 仅加载了 Benchmark 类和 Hooks 类本身 | 极早期的环境准备、常量定义(注意此时几乎什么都还不可用) |
pre_controller | 任何控制器被调用之前 | 所有基类、路由解析、安全检查均已完毕 | 统一鉴权、请求预处理 |
post_controller_constructor | 控制器被实例化之后、任何方法调用之前 | 控制器构造函数已执行 | 向控制器注入公共数据、设置视图变量 |
post_controller | 控制器完全执行完毕之后 | 控制器方法已执行 | 结果后处理、日志记录 |
display_override | 覆盖_display()方法 | 最终页面已生成,尚未发送给浏览器 | 用自己的方式输出页面(见 6.1 节) |
cache_override | 覆盖_display_cache()方法 | 尚未检查缓存 | 用自定义缓存展示机制替代内置缓存 |
post_system | 最终渲染页面已发送给浏览器之后 | 整个系统执行结束 | 收尾清理、性能统计 |
下面结合 system/core/CodeIgniter.php 中的实际调用点,逐一确认这些时机的源码依据。
6.1pre_system:最早的一个节点
钩子类在框架初始化早期就被实例化,紧接着立刻触发pre_system(system/core/CodeIgniter.php):
$EXT =& load_class('Hooks', 'core', $CFG); // ... $EXT->call_hook('pre_system');文档特别提醒:"Only the benchmark and hooks class have been loaded at this point. No routing or other processes have happened."此时基准测试类和钩子类是仅有的已加载组件,路由、安全、输入过滤等都还没有发生。因此在这个 Hook 点里你拿不到CI_URI、CI_Router等对象,只能做最基础的环境准备(例如根据请求设置自定义常量、修改$_SERVER等)。
6.2pre_controller:控制器调用之前
在控制器实例化之前,路由解析和缓存检查早已完成(缓存命中会直接exit)。代码中的调用位置(system/core/CodeIgniter.php):
$EXT->call_hook('pre_controller');此时*"All base classes, routing, and security checks have been done"*——所有基类、路由与安全检查均已完成,是执行全局鉴权、请求级别预处理的最佳时机。
6.3post_controller_constructor:实例化之后、方法调用之前
控制器通过$CI = new $class();完成实例化后,立即触发该钩子(system/core/CodeIgniter.php):
$CI = new $class(); // ... $EXT->call_hook('post_controller_constructor');此时控制器构造函数已执行完毕,但控制器方法尚未被调用(后续才执行call_user_func_array(array(&$CI, $method), $params),见 system/core/CodeIgniter.php)。适合做"所有控制器共用"的初始化:比如批量设置视图数据、加载公共辅助函数。
6.4post_controller:控制器执行完毕
控制器方法执行并完成基准测试标记后触发(system/core/CodeIgniter.php):
$BM->mark('controller_execution_time_( '.$class.' / '.$method.' )_end'); // ... $EXT->call_hook('post_controller');"Called immediately after your controller is fully executed."适合做请求日志、业务结果的后处理。
6.5display_override:接管页面输出
默认情况下,框架在post_controller之后调用$OUT->_display()把最终页面发送给浏览器。而display_override钩子可以完全覆盖这一行为(system/core/CodeIgniter.php):
if ($EXT->call_hook('display_override') === FALSE) { $OUT->_display(); }也就是说:一旦你定义了display_override钩子且其执行成功,框架不再调用内置的_display(),改用你自己的输出方式。文档给出了标准的取值方法:
$this->CI =& get_instance(); // 最终渲染数据通过 Output 组件的 get_output() 获取 $data = $this->CI->output->get_output();get_output()是 system/core/Output.php 中定义的公开方法,用于取回已渲染的最终输出字符串。典型场景:把页面内容写入模板引擎、压缩后再输出、或写入自定义响应头。注意display_override是一个"override"型钩子,与普通钩子不同——框架关心它的返回值来判断是否继续走内置逻辑。
6.6cache_override:接管缓存展示
在系统启动阶段,框架会先检查是否存在可用的缓存文件;cache_override允许你用自定义方法替代内置的_display_cache()(system/core/CodeIgniter.php):
if ($EXT->call_hook('cache_override') === FALSE && $OUT->_display_cache($CFG, $URI) === TRUE) { exit; }只有当钩子返回FALSE(即未定义或执行失败)时,框架才会回落到内置的_display_cache()(定义于 system/core/Output.php)。这让你可以用自己的缓存展示机制,例如从 Redis 而非文件缓存中读取并输出页面。相关背景可参考 Output 库文档。
6.7post_system:系统收尾
页面发送完毕、整个系统执行结束后触发(system/core/CodeIgniter.php):
$EXT->call_hook('post_system');文档描述为:"Called after the final rendered page is sent to the browser, at the end of system execution."适合做收尾工作,如统计本次请求耗时、释放外部资源等。
七、源码级执行细节与注意事项
7.1 钩子脚本的加载与实例化缓存
在_run_hook()中,类形式的钩子会被缓存复用(system/core/Hooks.php):首次触发时require_once脚本并new $class(),实例存入$this->_objects[$class];后续同一类再次触发时直接从缓存取对象调用方法,避免重复实例化。同时框架会校验class_exists()与method_exists(),不满足则返回FALSE。
7.2 防止无限循环的_in_progress标志
_run_hook()中有一个防递归保护(system/core/Hooks.php):
// Safety - Prevents run-away loops // If the script being called happens to have the same // hook call within it a loop can happen if ($this->_in_progress === TRUE) { return; }如果某个钩子脚本内部又触发了同名钩子(比如在pre_controller钩子里再次调用会触发pre_controller的代码),_in_progress标志会阻止其无限递归,这是一个值得了解的安全设计。
7.3 环境专属的钩子定义文件
除了通用的application/config/hooks.php,框架还支持按环境加载application/config/{ENVIRONMENT}/hooks.php(见 system/core/Hooks.php)。两个文件都会被加载,可用来区分开发/生产环境的不同钩子配置。ENVIRONMENT常量由index.php定义(通常为development/testing/production)。
7.4 测试中的钩子注册方式
在仓库的测试框架中,Hooks 类被注册为核心扩展之一:tests/mocks/ci_testcase.php里的$config['core_classes']['hooks'] = 'ext'将CI_Hooks映射为可被 mock 的扩展类。这从侧面印证了钩子类与load_class()体系的关系——任何第三方库都可以通过类似方式扩展或替换核心类,这也是"不 hacking 核心文件"这一设计哲学的一部分。
八、实战示例:完整的钩子落地流程
综合以上内容,一个完整的钩子使用流程如下:
第 1 步:启用钩子。在 application/config/config.php 中设置$config['enable_hooks'] = TRUE;。
第 2 步:编写钩子脚本。在application/hooks/下创建Myclass.php:
<?php defined('BASEPATH') OR exit('No direct script access allowed'); class MyClass { public function Myfunction($params) { // 例如:记录钩子触发时收到的参数 log_message('debug', 'Hook triggered with: '.json_encode($params)); } }第 3 步:注册钩子。在 application/config/hooks.php 中追加:
$hook['pre_controller'] = array( 'class' => 'MyClass', 'function' => 'Myfunction', 'filename' => 'Myclass.php', 'filepath' => 'hooks', 'params' => array('beer', 'wine', 'snacks') );第 4 步:验证。触发任意请求,观察日志中是否出现 "Hook triggered with" 记录;若同时需要多个动作,可改用$hook['pre_controller'][] = ...追加更多钩子。
注意事项速查:
- 脚本必须位于
application/内部,filepath不带尾部斜杠; function必填,class可选(留空即按全局函数处理);params会作为单个参数整体传入;- 同一 Hook 点多脚本时,定义顺序即执行顺序;
display_override与cache_override属于 override 型钩子,框架依据其返回值决定是否回落内置逻辑;pre_system阶段组件极少,只能访问 Benchmark 与 Hooks 自身。
九、小结
Hooks 是 CodeIgniter 框架"可扩展而不破坏核心"理念的直接体现:通过enable_hooks一个开关、application/config/hooks.php一个文件,以及七个精心设计的 Hook 点,你可以在框架生命周期的任意关键节点插入自己的逻辑。理解 system/core/Hooks.php 中call_hook()/_run_hook()的实现,以及 system/core/CodeIgniter.php 中各处call_hook()的调用位置,就能精准把握每个节点的触发时机与可用资源,从而写出既安全又高效的钩子代码。无论是全局鉴权、视图数据注入、输出后处理,还是缓存机制定制,Hooks 都是首选且官方推荐的实现路径。
【免费下载链接】CodeIgniterOpen Source PHP Framework (originally from EllisLab)项目地址: https://gitcode.com/gh_mirrors/co/CodeIgniter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考