在复杂的Web应用中,中间件和事件系统是解耦业务逻辑的两大神器。ThinkPHP 6+内置了强大的中间件机制和事件系统,让开发者可以轻松实现请求过滤、权限验证、日志记录等横切关注点。本文通过构建一个API鉴权与请求日志系统,完整演示如何利用中间件和事件系统构建可扩展的架构。
一、为什么需要中间件和事件系统?
传统方式中,鉴权和日志逻辑散落在控制器中,导致代码重复且难以维护。中间件可以在请求到达控制器之前执行通用逻辑,事件系统则能在特定时刻触发异步操作(如记录日志、发送通知)。两者结合,可以让控制器专注于业务逻辑。
二、项目结构设计
我们基于ThinkPHP 6+创建项目,目录结构如下:
├─ app
│ ├─ controller
│ │ ├─ Index.php
│ │ └─ User.php
│ ├─ middleware
│ │ ├─ AuthMiddleware.php # 鉴权中间件
│ │ └─ LogMiddleware.php # 日志中间件
│ ├─ event
│ │ ├─ RequestEvent.php # 请求事件类
│ │ └─ RequestListener.php # 请求事件监听器
│ ├─ model
│ │ └─ User.php
│ └─ service
│ └─ AuthService.php # 鉴权服务
├─ config
│ └─ middleware.php # 中间件配置
├─ route
│ └─ app.php # 路由定义
└─ .env
三、中间件实战:API鉴权
1. 创建鉴权中间件
<?php
namespace appmiddleware;
use appserviceAuthService;
use thinkRequest;
use thinkResponse;
class AuthMiddleware
{
/**
* 处理请求
* @param Request $request
* @param Closure $next
* @return Response
*/
public function handle(Request $request, Closure $next)
{
// 获取token(从Header或参数中)
$token = $request->header('Authorization');
if (!$token) {
$token = $request->param('token');
}
if (!$token) {
return json(['code' => 401, 'message' => '未提供认证令牌'], 401);
}
// 调用鉴权服务验证token
$authService = new AuthService();
$userInfo = $authService->verifyToken($token);
if (!$userInfo) {
return json(['code' => 401, 'message' => '令牌无效或已过期'], 401);
}
// 将用户信息绑定到请求对象,方便后续控制器使用
$request->userInfo = $userInfo;
// 继续执行请求
return $next($request);
}
}
2. 鉴权服务类
<?php
namespace appservice;
use appmodelUser;
use FirebaseJWTJWT;
use FirebaseJWTKey;
class AuthService
{
private string $secretKey = 'your-secret-key-here';
/**
* 生成JWT令牌
*/
public function generateToken(array $userInfo): string
{
$payload = [
'iss' => 'thinkphp-api', // 签发者
'iat' => time(), // 签发时间
'exp' => time() + 7200, // 过期时间2小时
'uid' => $userInfo['id'],
'username' => $userInfo['username']
];
return JWT::encode($payload, $this->secretKey, 'HS256');
}
/**
* 验证JWT令牌
*/
public function verifyToken(string $token): ?array
{
try {
$decoded = JWT::decode($token, new Key($this->secretKey, 'HS256'));
return (array) $decoded;
} catch (Exception $e) {
return null;
}
}
}
3. 路由配置与中间件注册
// route/app.php
use thinkfacadeRoute;
// 公开路由(无需登录)
Route::post('login', 'User/login');
// 需要鉴权的路由组
Route::group('api', function () {
Route::get('user/info', 'User/getUserInfo');
Route::post('user/update', 'User/update');
})->middleware(appmiddlewareAuthMiddleware::class);
四、事件系统实战:请求日志
我们希望每次API请求都记录日志,但不希望在中间件或控制器中直接写日志代码。使用事件系统可以解耦。
1. 定义请求事件类
<?php
namespace appevent;
use thinkRequest;
class RequestEvent
{
public Request $request;
public array $responseData;
public float $startTime;
public float $endTime;
public function __construct(Request $request, array $responseData, float $startTime, float $endTime)
{
$this->request = $request;
$this->responseData = $responseData;
$this->startTime = $startTime;
$this->endTime = $endTime;
}
}
2. 创建监听器
<?php
namespace appevent;
use thinkfacadeLog;
class RequestListener
{
/**
* 处理请求日志
*/
public function handle(RequestEvent $event): void
{
$request = $event->request;
$duration = round(($event->endTime - $event->startTime) * 1000, 2); // 毫秒
$logData = [
'method' => $request->method(),
'url' => $request->url(true),
'params' => $request->param(),
'response' => $event->responseData,
'duration' => $duration . 'ms',
'timestamp' => date('Y-m-d H:i:s'),
'user_id' => $request->userInfo['uid'] ?? 0,
];
// 写入日志文件
Log::write(json_encode($logData, JSON_UNESCAPED_UNICODE), 'request');
// 也可以写入数据库或发送到消息队列
// Db::name('request_log')->insert($logData);
}
}
3. 注册事件与监听器
// config/event.php
return [
'bind' => [
'RequestEvent' => 'appeventRequestEvent',
],
'listen' => [
'RequestEvent' => [
'appeventRequestListener',
],
],
];
4. 在控制器中触发事件
<?php
namespace appcontroller;
use appeventRequestEvent;
use thinkfacadeEvent;
use thinkRequest;
class User extends BaseController
{
public function getUserInfo(Request $request)
{
$startTime = microtime(true);
// 业务逻辑
$data = ['username' => $request->userInfo['username'], 'email' => 'test@example.com'];
$endTime = microtime(true);
// 触发请求日志事件
Event::trigger('RequestEvent', new RequestEvent($request, $data, $startTime, $endTime));
return json(['code' => 0, 'data' => $data]);
}
}
五、中间件与事件协同工作流
整个请求流程如下:
- 客户端发送请求到
/api/user/info AuthMiddleware拦截请求,验证JWT令牌,将用户信息绑定到$request->userInfo- 请求到达
UserController::getUserInfo,执行业务逻辑 - 控制器触发
RequestEvent事件,传递请求和响应数据 RequestListener监听事件,将日志写入文件或数据库- 返回JSON响应给客户端
这样,鉴权和日志逻辑被完美解耦,控制器只需关注业务,中间件关注横切关注点,事件系统处理异步操作。
六、扩展:使用中间件记录请求耗时
我们还可以创建一个专门的日志中间件,记录所有请求的耗时,而不需要每个控制器手动触发事件。
<?php
namespace appmiddleware;
use thinkRequest;
use thinkResponse;
use thinkfacadeLog;
class LogMiddleware
{
public function handle(Request $request, Closure $next)
{
$startTime = microtime(true);
// 执行请求
$response = $next($request);
$endTime = microtime(true);
$duration = round(($endTime - $startTime) * 1000, 2);
// 记录日志
Log::write(sprintf(
'[%s] %s %s - %dms',
$request->method(),
$request->url(true),
$response->getCode(),
$duration
), 'access');
return $response;
}
}
在 config/middleware.php 中注册为全局中间件:
return [
'global' => [
appmiddlewareLogMiddleware::class,
],
];
这样所有请求都会自动记录耗时日志,无需修改任何控制器代码。
七、最佳实践与注意事项
- 中间件顺序:在路由中注册中间件时,注意顺序。例如鉴权中间件应该在日志中间件之后,因为日志可能需要记录用户信息。
- 事件异步化:如果日志写入数据库较慢,可以使用消息队列(如Redis)异步处理,避免阻塞响应。
- 依赖注入:中间件和监听器支持依赖注入,可以方便地使用模型、服务等。
- 异常处理:在中间件中抛出异常会被ThinkPHP的异常处理机制捕获,可以统一返回JSON错误。
八、总结
通过中间件和事件系统,我们构建了一个可扩展的API鉴权与日志体系。中间件负责请求过滤,事件系统负责解耦业务逻辑。这套架构可以轻松扩展到更多场景:如接口频率限制、权限验证、数据校验等。
ThinkPHP的中间件和事件系统设计优雅,掌握它们能让你的应用架构更清晰、更易维护。现在就去重构你的项目吧!
本文为原创技术教程,代码基于ThinkPHP 6.1+测试通过。建议在实际项目中结合Composer安装JWT库:composer require firebase/php-jwt

