CodeIgniter Hooks 钩子机制完全指南:在不改动核心文件的前提下扩展框架执行流程
2026/9/21 18:01:59 网站建设 项目流程

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_systempost_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_hooksTRUE时,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_URICI_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_overridecache_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询