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 函数也会返回一个组数, 里面含有所有行为函数的结果,但索引是数字索引, 要检索哪个行为的对应的结果,需要知道那个行为是第几个调用..这个就很麻烦了.
- FA的JS调用机制说明
- FA的JS之Fast.api逐个详解
- FA页面渲染时后端传递数据给前端的方式
- FA的ajax查询数据的前后台流程
- FA特有的函数解释
- FA的鉴权Auth类
- extend\fast\Auth.php详解
- application\admin\library\Auth.php详解
- application\common\library\Auth.php详解
- FA的Token机制
- FA管理员(后台)的权限机制
- FA用户(前台和API)的权限机制
- FA在前台模板文件中进行鉴权
- FA的登录页面
- TP类Hook:钩子机制
- TP类Lang:多语言机制
- TP类Config:参数配置机制
- TP类Request:请求类
- TP的模型关联详解
- think-queue队列组件
- Queue.php
- \queue\Connector.php
- \queue\connector\Redis.php
- \queue\Job.php
- queue\job\Redis.php
- PHP规则:正则表达式
- PHP规则:闭包与匿名函数
- 项目架构说明
- 代码架构
- TP数据库where条件的各种写法
