💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、豆包、星火、月之暗面及文生图、文生视频 广告
thinkphp5 的钩子解释: 官方文档解释: **行为和钩子** ThinkPHP 中的行为是一个比较抽象的概念,你可以把行为想象成在应用执行过程中的一个动作。在框架的执行流程中,例如路由检测是一个行为,静态缓存是一个行为,用户权限检测也是行为,大到业务逻辑,小到浏览器检测、多语言检测等等都可以当做是一个行为,甚至说你希望给你的网站用户的第一次访问弹出 `Hello,world!` 这些都可以看成是一种行为,把这些行为抽离出来的目的是为了让你无需改动框架和应用,而在外围通过扩展或者配置来改变或者增加一些功能。 而不同的行为之间也具有位置共同性,比如,有些行为的作用位置都是在应用执行前,有些行为都是在模板输出之后,我们把这些行为发生作用的位置称之为 **钩子** ,当应用程序运行到这个**钩子**的时候,就会被拦截下来,统一执行相关的行为,类似于 `AOP` 编程中的“切面”的概念,给某一个**钩子**绑定相关行为就成了一种类 `AOP` 编程的思想。 一个**钩子**可以注册多个行为,执行到某个**钩子**位置后,会按照注册的顺序依次执行相关的行为。但在某些特殊的情况下,你可以设置某个**钩子**只能执行一次行为,又或者你可以在一个**钩子**的某个行为中返回 `false` 来强制终止后续的行为执行;一个行为可以同时注册到多个不同的**钩子**上,完全看应用的需求来设计。 我们来具体解释这个官方解释: **行为是什么** : 我们一般编程时,当 A 函数运行到某个地方,这个 A 函数需要调用另外一个 B 函数,B 函数是专门处理某个功能的; 而这个 B 函数就可以看作是一个行为; **钩子是什么** : 有时候还需要多个行为一起运行,那么多个行为合起来就形成了一个钩子.一个钩子也可以只有一个行为; *我们把这些行为发生作用的位置称之为*钩子 : 官方这句话并不是在定义钩子; 位置并不能称之为一个实际的东西,位置只是用来说明我们要在哪里触发这个钩子,执行钩子里面的行为函数;所以我认为钩子是一个有实际意义的东西,它就行为的集合,从代码上来看,这个钩子 tags 就是一个数组类型;代码的定义:`private static $tags = [];` 在 TP 里面就是这样的一个层次结构; **为什么设计这样一个结构** : 原因 1:官方原话:把这些行为抽离出来的目的是为了让你无需改动框架和应用,而在外围通过扩展或者配置来改变或者增加一些功能; 每一个行为就是一个 php 文件,里面是一个类.你可以任意增加修改里面的代码,这样不需要去修改原来的上级代码;如果是按一般的写法,要修改函数 B,就要动原来的代码了.有时候不想动框架层次的代码,这样抽离出来就实现了不动框架目的; 原因 2:这些行为有可能会在不同的地方重复使用多次,抽离出来也就减少了重复代码,跟函数复用是一样的道理; 原因 3:有钩子这一层的话,那么就可以任意组合各种行为,钩子是一个数组,里面存放行为,执行时,按数组数字索引顺序执行; 原因 4:和原因 1 是差不多的,这样设计,原开发者可以在各个方法中,预写好一些钩子,定好钩子名称和功能说明,二开的人就可以直接使用. **如何使用钩子:** 标准流程: 1:在 application\tags.php,这个文件里面是系统级别的钩子, 在 thinkphp\library\think\App.php 的 `if (is_file(CONF_PATH . $module . 'tags' . EXT))` 中自动加载这个文件里面的内容进入 Hook 类,`$module` 在这里是空值,所以组合出来的文件路径就是:public/../application/tags.php ; 2:每一个模块也可以有自己的 tags.php 文件,例如:application\admin\tags.php ,也会按第一步那样自动加载,只不过 `$module` 值是当前模块名称 admin/,TP 自动加上了连接符(DS:): `$module = $module ? $module . DS : ''; ` *DS 是指 :DIRECTORY_SEPARATOR,是一个 PHP 内置常量 ,表示目录连接符,在 windows 下的是\和/,而 LINUX 下的是/ ;* *为了消除这种差异,PHP 就内置了这个常量,自动识别当前运行环境下的目录连接符是哪个.如果在代码中写死连接符,那么换个系统平台代码就可能报错了.各种目录/文件不存在的错误;* 3:tags.php 文件的写法: tags.php 里面就是一个数组, 下面是一个例子: ```php return [ // 应用结束 'app_end' => [ 'app\\admin\\behavior\\AdminLog', ], ]; ``` 我们去 application\admin\behavior\AdminLog.php 打开这个文件,里面是: ```php class AdminLog { public function run(&$params) { //只记录POST请求的日志 if (request()->isPost() && config('fastadmin.auto_record_log')) { \app\admin\model\AdminLog::record(); } } } ``` 里面只有一个方法 叫 run,TP 规定,如果一个行为类里面只有一个方法,那么就默认叫 run. 在 thinkphp\library\think\Hook.php 的 ``` $method = ($tag && is_callable([$obj, $method])) ? $method : 'run'; ``` 写明了,如果这个方法名不是代表一个可执行的函数,就把名字换成 run ; 同时也就是说 如果这个方法名代表的函数不存在,就默认执行 run 函数; 运行这个 run,可以看到其实是又去调用模型类 AdminLog 的 record 方法:记录一条日志到数据; **那么在什么地方会执行这个钩子呢?** 搜索一下代码,发现是 thinkphp\library\think\App.php 的 run 函数里面: ``` // 监听 app_end Hook::listen('app_end', $response); ``` app 的 run 函数就是整个系统的业务总执行函数,后面所有的代码都是由这个函数调起的; 所以在这个函数 最后 return 之前,也就是整个业务代码已经全部执行完毕了, 执行 app_end 钩子的行为; 以上是钩子的创建配置,载入,监听执行的整个过程; 下面介绍一些具体细节的使用注意事项: **怎么写 tags.php 文件:** ``` 'app_end' => [ 'app\\admin\\behavior\\AdminLog', ], ``` '钩子名称' =>[ ``` '行为类文件的路径' ``` ] **一个行为类里面有多个行为函数怎么绑定钩子:** 我们来看 application\tags.php; ``` // 应用调度 'app_dispatch' => [ 'app\\common\\behavior\\Common', ], // 模块初始化 'module_init' => [ 'app\\common\\behavior\\Common', ], // 插件开始 'addon_begin' => [ 'app\\common\\behavior\\Common', ], ``` 它里面 3 个钩子,里面绑定的行为类是同一个,然后再看这个 application\common\behavior\Common.php, `public function appDispatch(&$dispatch)` `public function moduleInit(&$request)` `public function addonBegin(&$request)` Common.php 里面有 3 个函数 ,刚好对应 tags 的 3 个钩子,命名就是驼峰法,一一对应. 在触发钩子执行行为函数时,TP 会把钩子名称进行转化成驼峰形式 函数名字转化发生在 thinkphp\library\think\Hook.php 的 ` $method = Loader::parseName($tag, 1, false);` 如果在行为类中找不到对应名字的函数,则默认调用 run 函数; 所以如果想要一个行为类中放多个行为函数,则需要按照钩子名称用驼峰法命名行为函数名字.这样一一对应起来; 如果想让这个行为函数可以绑定任意钩子, 就命名为 run; 行为类的命名没有要求,但为了规范,一般是按照模型的名字来命名,这样一眼就能理解这个行为类大概是干什么的. 行为类不需要继承任何类,只需要定好命名空间,use 引入需要用到的类即可; Hook类: `namespace think;` `class Hook` ``` /** * @var array 标签 */ private static $tags = []; ``` $tags数组是用才存储行为索引数据的,是一个二维数组, 第一维的索引是行为分组名称,第二维是数字索引,值是一个可以表示行为内容的东西,这个东西可以是字符串,可以是匿名函数 数组结构如下: ``` $tags = [ '组名':[ 0:'app\\index\\behavior\\CheckAuth' 1:匿名函数 ] ]; ``` **不使用 tags.php 文件,动态绑定行为add():** tags.php 文件配置好的钩子数组,是使用 hook 的 import 方法批量载入的,import 函数其实也是调用 add 函数的; 而使用 hook 的 add 函数, 可以直接添加行为. ``` /** * 动态添加行为扩展到某个标签 * @access public * @param string $tag 标签名称 * @param mixed $behavior 行为名称 * @param bool $first 是否放到开头执行 * @return void */ public static function add($tag, $behavior, $first = false) { //研究测试Log::record('[ hook->add ] ' . json_encode($tag) , 'info'); //如果不存在这个tag,则初始化这个tag为空数组 isset(self::$tags[$tag]) || self::$tags[$tag] = []; if (is_array($behavior) && !is_callable($behavior)) { //如果_overlay为真, 就会把这个钩子里面的原有的行为都清除掉,只保留新插入的这个 if (!array_key_exists('_overlay', $behavior) || !$behavior['_overlay']) { unset($behavior['_overlay']); self::$tags[$tag] = array_merge(self::$tags[$tag], $behavior); } else { unset($behavior['_overlay']); self::$tags[$tag] = $behavior; } } elseif ($first) { //array_unshift — 在数组开头插入一个或多个单元 array_unshift(self::$tags[$tag], $behavior); } else { self::$tags[$tag][] = $behavior; } } ``` $tag:钩子的名称 ; $behavior:行为, 这里可以是一个行为类的'路径',如 tags.php 写的,是一个字符串,然后通过转换之后,找得到对应的函数,也是可以的; 也可以是一个匿名函数,也就是说这个 $behavior:可以是一个 `is_callable` 的东西,是一个函数; 还可以是 数字索引数组,然后这个数组的结构是:`第一个元素是类名,第二个元素是函数名;` 还可以是一个对象 obj,但是这个 obj 里面必须要有与钩子名称驼峰写法对应的方法,否则无法找到方法进行调用; 还可以是一个字符串里面带有 双冒号:: 的. 这种形式默认为 调用某个类的静态函数, 例如:App::run() ,*这种写法就没过人用...* $first:行为函数的调用顺序,默认是先加的先调用,如果想后加的先调用,就传 true 过来, 就会把这个函数放到数组的第一位; 在配置钩子的行为函数时,可以把这个钩子里面的其他函数清除掉,只留下这个新加入的行为; ``` 'app_init'=> [ 'app\\index\\behavior\\CheckAuth', '_overlay'=>true ], ``` 加入_overlay 元素,设置为 真,即可; 如果不存在_overlay 或者_overlay 不是代表真,那么就不会清除钩子原有的行为; **批量导入行为类import():** ``` /** * 批量导入插件 * @access public * @param array $tags 插件信息 * @param boolean $recursive 是否递归合并 * @return void */ public static function import(array $tags, $recursive = true) { //研究测试Log::record('[ hook->import ] ' . json_encode($tags) , 'info'); if ($recursive) { //默认都是需要使用add方法进行添加,因为add方法进行了有效性过滤, //如果你强制不进行递归合并,那么只是简单地数组相加,那么你必须自己保证数组内容没有错误! foreach ($tags as $tag => $behavior) { self::add($tag, $behavior); } } else { //以新的数组为准,同时也把旧数组有的,而新数组没有的保留下来 self::$tags = $tags + self::$tags; } } ``` **获取行为类信息get():** 如果不传参数,则获取整个行为数组; ``` /** * 获取插件信息 * @access public * @param string $tag 插件位置(留空获取全部) * @return array */ public static function get($tag = '') { if (empty($tag)) { return self::$tags; } return array_key_exists($tag, self::$tags) ? self::$tags[$tag] : []; } ``` **钩子是如何执行行为函数的exec():** 执行某个行为是使用 hook 的 exec 函数 ``` /** * 执行某个行为 * @access public * @param mixed $class 要执行的行为 * @param string $tag 方法名(标签名) * @param mixed $params 传人的参数 * @param mixed $extra 额外参数 * @return mixed */ public static function exec($class, $tag = '', &$params = null, $extra = null) { //研究测试 Log::record('[ hook->exec ] ' . $tag , 'info'); App::$debug && Debug::remark('behavior_start', 'time'); $method = Loader::parseName($tag, 1, false); //研究测试 Log::record('[ BEHAVIOR2 ] ' . $method , 'info'); //研究测试Log::record('[ hook->exec->method ] ' . $method , 'info'); if ($class instanceof \Closure) { //call_user_func_array — 调用回调函数,并把一个数组参数作为回调函数的参数 $result = call_user_func_array($class, [ & $params, $extra]); $class = 'Closure'; } elseif (is_array($class)) { //list — 把数组中的值赋给一组变量, //这里默认$class是一个有2个元素的数组,并且第一个元素是类名,第二个元素是函数名 list($class, $method) = $class; $result = (new $class())->$method($params, $extra); $class = $class . '->' . $method; } elseif (is_object($class)) { $result = $class->$method($params, $extra); $class = get_class($class); } elseif (strpos($class, '::')) { //直接调用某个类的静态函数 $result = call_user_func_array($class, [ & $params, $extra]); } else { $obj = new $class(); //如果找不到对应钩子名称的驼峰命名的函数,就默认调用run函数 $method = ($tag && is_callable([$obj, $method])) ? $method : 'run'; $result = $obj->$method($params, $extra); } if (App::$debug) { Debug::remark('behavior_end', 'time'); Log::record('[ BEHAVIOR ] Run ' . $class . ' @' . $tag . ' [ RunTime:' . Debug::getRangeTime('behavior_start', 'behavior_end') . 's ]', 'info'); } return $result; } ``` 首先判断这个 $class 是不是一个[匿名函数类](https://gitee.com/link?target=https://www.php.net/manual/zh/class.closure.php), *了解 php 的[匿名函数](https://gitee.com/link?target=https://www.php.net/manual/zh/functions.anonymous.php)的实现方式* `if ($class instanceof \Closure)` 如果是一个匿名函数类, 就直接调用; ``` $result = call_user_func_array($class, [ & $params, $extra]); ``` 然后判断是不是一个数组,这个数组结构必须是 `第一个元素是类名,第二个元素是函数名` ``` list($class, $method) = $class; $result = (new $class())->$method($params, $extra); ``` 然后判断是不是一个 对象,如果是就直接调用 ``` elseif (is_object($class)) { $result = $class->$method($params, $extra); ``` 然后判断 这个 class 是不是一个带有双冒号的 字符串 `elseif (strpos($class, '::'))` 如果是就直接调用 `$result = call_user_func_array($class, [ & $params, $extra]);` 如果不是上面的任何一种,那么就当作是默认的形式,class 是一个 类的'路径' ` $obj = new $class();` 然后判断是否存在对应的驼峰命名的行为函数,如果没有就 调用 run 函数; ``` $method = ($tag && is_callable([$obj, $method])) ? $method : 'run'; $result = $obj->$method($params, $extra); ``` **如何设置监听listen():** 所谓的设置监听,其实就是执行listen()函数,然后找到要执行的行为组,再把组里面的所有的行为函数都执行一遍. 通过调用一个函数,达到调用N个函数的目的; ``` /** * 监听标签的行为 * @access public * @param string $tag 标签名称 * @param mixed $params 传入参数 * @param mixed $extra 额外参数 * @param bool $once 只获取一个有效返回值 * @return mixed */ public static function listen($tag, &$params = null, $extra = null, $once = false) { $results = []; foreach (static::get($tag) as $key => $name) { $results[$key] = self::exec($name, $tag, $params, $extra); // 如果返回 false,或者仅获取一个有效返回则中断行为执行 if (false === $results[$key] || (!is_null($results[$key]) && $once)) { break; } } //end() 函数将内部指针指向数组中的最后一个元素,并输出。 return $once ? end($results) : $results; } ``` **如何中断钩子的执行** 如果行为函数返回 false ,则会中断钩子的执行,后面的行为函数将不会继续执行; **获得一个有效返回后就中断钩子的执行** 在设置监听的时候,listen 函数的参数中 $once 的值传 true 或者真值, 那么当 行为函数有返回值时(这个返回值可以是 false 或者假,只要不是 null,`(!is_null($results[$key]) && $once)`), 就会中断钩子的执行,后面的行为函数将不会继续执行; listen 返回最后一个行为函数返回的值 :`return $once ? end($results) : $results; //end() 函数将内部指针指向数组中的最后一个元素,并输出。` 在实际应用中, 大多数行为函数都是无返回值的, 直接是 return ; , 后面不带任何东西;也就是返回的是 null; 这些行为函数大多数都是一些程序的扩展,是一些枝条, 并不会对主程序产生影响. 当然 listen 函数也会返回一个组数, 里面含有所有行为函数的结果,但索引是数字索引, 要检索哪个行为的对应的结果,需要知道那个行为是第几个调用..这个就很麻烦了.