在构建现代Web API时,如何处理请求鉴权、日志记录、参数过滤和响应格式化等横切关注点,往往决定了项目的维护性和扩展性。ThinkPHP 8 提供了强大且灵活的中间件与事件系统,让开发者可以将这些通用逻辑从业务控制器中剥离出来,形成清晰的请求处理管道。本文将通过一个完整的案例,手把手教你如何利用ThinkPHP 8的中间件和事件机制,构建一个具备日志、权限控制和数据转换功能的API应用,代码简洁且高度可扩展。
一、ThinkPHP 8 中间件基础
中间件是进入控制器前后的“关卡”,可以执行前置操作(如验证token)和后置操作(如添加响应头)。ThinkPHP 8 支持全局中间件、路由中间件和控制器中间件。定义中间件非常简单,只需实现 thinkMiddleware 接口或者使用 handle 方法。
一个典型的中间件结构如下:
<?php
namespace appmiddleware;
class CheckToken
{
public function handle($request, Closure $next)
{
// 前置校验
if (!$request->header('Authorization')) {
return json(['code' => 401, 'msg' => '缺少令牌'])->code(401);
}
// 继续执行后续中间件与控制器
$response = $next($request);
// 后置处理(可选)
$response->header('X-Powered-By', 'ThinkPHP8');
return $response;
}
}
中间件通过 $next($request) 将请求传递给管道的下一阶段,这种洋葱模型让逻辑嵌套变得非常自然。
二、创建记录API访问日志的中间件
我们的需求是:记录每一次API请求的方法、路径、参数、执行时间和响应状态码。这需要前置记录开始时间,后置计算耗时并写入日志。ThinkPHP 8 内置了强大的日志系统,可以直接使用。
步骤1:创建中间件文件
在 app/middleware 目录下新建 ApiLogMiddleware.php:
<?php
namespace appmiddleware;
use thinkfacadeLog;
class ApiLogMiddleware
{
public function handle($request, Closure $next)
{
// 前置:记录开始时间
$startTime = microtime(true);
$method = $request->method();
$path = $request->pathinfo();
$params = $request->param();
// 执行后续处理
$response = $next($request);
// 后置:计算耗时并写入日志
$duration = round(microtime(true) - $startTime, 4) * 1000; // 毫秒
$statusCode = $response->getCode();
$logMsg = sprintf(
'[API] %s %s | 参数: %s | 状态码: %d | 耗时: %.2fms',
$method,
$path,
json_encode($params, JSON_UNESCAPED_UNICODE),
$statusCode,
$duration
);
Log::info($logMsg);
return $response;
}
}
该中间件以非侵入方式记录了每个请求的核心信息,控制器无需任何改动。
三、事件系统:实现业务逻辑解耦
ThinkPHP 8 的事件系统遵循典型的观察者模式。它可以让你在用户登录、下单、支付等核心操作发生后,触发多个独立的监听器,而无需修改核心业务代码。比如,我们希望在用户登录成功后,发送通知邮件并记录登录日志,但又不把这些逻辑写在登录方法中。
步骤1:定义事件类
在 app/event 目录下创建 UserLoggedIn.php:
<?php
namespace appevent;
class UserLoggedIn
{
public $userId;
public $loginTime;
public function __construct($userId)
{
$this->userId = $userId;
$this->loginTime = date('Y-m-d H:i:s');
}
}
步骤2:创建监听器
在 app/listener 下创建 SendLoginEmail.php:
<?php
namespace applistener;
use appeventUserLoggedIn;
use thinkfacadeLog;
class SendLoginEmail
{
public function handle(UserLoggedIn $event)
{
// 模拟发送邮件
Log::info("向用户 {$event->userId} 发送登录通知邮件");
// 真实场景调用邮件发送服务
}
}
再创建 RecordLoginLog.php:
<?php
namespace applistener;
use appeventUserLoggedIn;
use thinkfacadeDb;
class RecordLoginLog
{
public function handle(UserLoggedIn $event)
{
// 记录到数据库
Db::table('login_logs')->insert([
'user_id' => $event->userId,
'login_time' => $event->loginTime,
]);
}
}
步骤3:绑定事件与监听器
在应用的事件配置文件(通常为 app/event.php)中绑定:
<?php
return [
'bind' => [
// 其他绑定
],
'listen' => [
'UserLoggedIn' => [
applistenerSendLoginEmail::class,
applistenerRecordLoginLog::class,
]
],
];
当用户登录成功,业务代码只需要一行触发事件:
<?php
use appeventUserLoggedIn;
use thinkfacadeEvent;
class LoginController
{
public function login()
{
// 校验用户名密码(略)
$userId = 1001;
// 触发登录事件,监听器自动执行
Event::trigger(new UserLoggedIn($userId));
return json(['code' => 0, 'msg' => '登录成功']);
}
}
由此,登录核心逻辑保持纯粹,而通知和日志记录等附属功能可以通过事件灵活挂载,甚至后续新增监听器也无需修改原有代码。
四、打造完整的API请求管道:中间件接力
现在我们将中间件串联起来,构建一条完整的API处理管道:请求日志记录 → Token验证 → 控制器处理 → 响应处理。ThinkPHP 8 支持在路由或控制器层面指定多个中间件。
定义路由和中间件配置
在路由定义文件(如 route/app.php)中:
<?php
use thinkfacadeRoute;
Route::group('api', function () {
Route::get('user/profile', 'UserController/getProfile');
Route::post('user/update', 'UserController/updateProfile');
})->middleware([
appmiddlewareApiLogMiddleware::class, // 日志记录
appmiddlewareCheckToken::class, // 令牌验证
]);
或者,也可以直接在控制器中通过注解方式指定中间件(ThinkPHP 8 支持注解路由)。这样,所有进入 /api/ 的请求会先经过日志中间件,再经过令牌校验,最后到达控制器方法。如果令牌校验失败,请求根本不会进入控制器,同时日志中间件仍然会记录下这次失败的请求,因为日志记录在管道外侧。
五、综合案例:用户资料接口全流程
我们将以上内容整合,实现一个完整的用户资料接口。要求:
- 所有请求记录到日志。
- 请求头必须携带有效的
X-Token。 - 获取资料是一个耗时操作(模拟数据库查询),响应需要记录耗时。
- 资料查询完成后,触发一个“资料被访问”的事件,用于统计分析。
定义“资料被访问”事件与监听器
app/event/ProfileAccessed.php:
<?php
namespace appevent;
class ProfileAccessed
{
public $userId;
public $accessTime;
public function __construct($userId)
{
$this->userId = $userId;
$this->accessTime = date('Y-m-d H:i:s');
}
}
app/listener/StatAccessLog.php:
<?php
namespace applistener;
use appeventProfileAccessed;
use thinkfacadeLog;
class StatAccessLog
{
public function handle(ProfileAccessed $event)
{
Log::info("用户 {$event->userId} 的资料在 {$event->accessTime} 被访问");
// 此处可写入统计库或缓存
}
}
在 app/event.php 中注册监听。
创建用户控制器
<?php
namespace appcontroller;
use appeventProfileAccessed;
use thinkfacadeEvent;
use thinkfacadeDb;
class UserController
{
public function getProfile()
{
// 假设从token中解析出用户ID,这里模拟为1001
$userId = 1001;
// 模拟数据库查询(消耗时间)
$user = Db::table('users')->where('id', $userId)->find();
if (!$user) {
return json(['code' => 404, 'msg' => '用户不存在'])->code(404);
}
// 触发资料被访问事件
Event::trigger(new ProfileAccessed($userId));
return json([
'code' => 0,
'data' => [
'id' => $user['id'],
'name' => $user['name'],
'email' => $user['email'],
]
]);
}
}
这样,一个请求链路下来:ApiLogMiddleware 记录了请求信息与耗时,CheckToken 确保了安全性,控制器专注于业务并触发事件,而 StatAccessLog 监听器则默默地执行统计任务,完全解耦。
六、中间件中的请求与响应操作技巧
中间件不仅可以做权限检查,还可以对请求参数做预处理,或对响应做统一包装。例如,可以在一个后置中间件中将所有API响应的格式统一为:
{
"code": 0,
"message": "success",
"data": { ... }
}
实现这一点的中间件大致如下:
<?php
namespace appmiddleware;
class FormatResponse
{
public function handle($request, Closure $next)
{
$response = $next($request);
// 获取原始响应内容
$data = $response->getData();
// 如果控制器已经返回了统一格式,则跳过
if (isset($data['code'])) {
return $response;
}
// 统一包装
$newData = [
'code' => 0,
'message' => 'success',
'data' => $data,
];
return json($newData);
}
}
然后将此中间件添加到全局或路由组中,即可实现所有接口响应格式一致,而控制器只需返回 $data 部分,极大简化了开发。
七、事件系统的高级用法:延迟事件与事件订阅
对于非关键的任务,比如发送通知、更新统计,可以使用ThinkPHP 8 的延迟事件,将其推迟到请求结束后执行,避免阻塞用户响应。只需在监听器中实现 shouldQueue 方法或使用队列事件配置。此外,还可以使用事件订阅者将多个相关监听器整合到一个类中,便于管理。这些高级特性让事件系统能够轻松应对高并发场景下的异步处理需求。
八、总结
通过本文的实战案例,你已经掌握了ThinkPHP 8 中间件和事件系统的核心用法。中间件让我们能够以洋葱模型组织请求处理逻辑,将日志、鉴权、格式化等横切关注点从业务代码中抽离;事件系统则进一步解耦了核心业务与辅助逻辑,使应用变得更加模块化和可扩展。在实际项目中,合理搭配中间件管道与事件驱动,可以编写出干净、健壮且易于维护的API服务。现在,你可以在你的ThinkPHP 8项目中立即应用这些模式,体会架构改善带来的愉悦与高效。

