【基于 Swoole+Hyperf 的微服务实战】 第二周·周三:Hyperf 验证器与异常处理
今天主题是 Hyperf 验证器与异常处理。在构建 API 时,健壮的输入校验和统一的错误响应是专业性的体现。今天你将学会如何使用 hyperf/validation 组件为接口定义规则,并编写全局异常处理器,将任何错误都转化为美观的 JSON 响应,不再暴露丑陋的调试信息。

今日目标
- 掌握 Hyperf 验证器的安装、配置与基本用法。
- 能够为复杂的注册接口编写验证规则,并自定义错误消息。
- 理解 Hyperf 的异常处理流程,创建自定义异常处理器。
- 实现一个
ApiExceptionHandler,统一将所有异常格式化为 JSON 响应,并区分业务异常与系统异常。 - 结合验证器与异常处理,打造一个健壮的注册 API,并编写测试用例。
一、环境准备(约 20 分钟)
继续在 hyperf-app 项目中工作,确保热重启已开启(若未开启可执行 php bin/hyperf.php server:watch)。
cd swoole-course
docker-compose exec swoole bash
cd /var/www/hyperf-app
安装验证器组件
Hyperf 的验证器基于 Laravel 的 illuminate/validation,需要额外安装:
composer require hyperf/validation
安装完成后无需手动配置,框架会自动通过 hyperf/validation 的 ConfigProvider 注册相关服务。
发布验证器语言包(可选)
验证器错误消息默认是英文,我们可以发布中文语言包:
php bin/hyperf.php vendor:publish hyperf/translation
这会在 storage/languages/ 下生成语言文件。不过今天我们先使用自定义错误消息,后续再深入国际化。
二、知识核心:验证器与异常处理模型(约 1 小时)
1. 验证器工作原理
Hyperf 验证器通过验证器工厂 (Hyperf\Validation\Contract\ValidatorFactoryInterface) 创建验证实例。你可以注入该工厂,然后使用 make() 方法:
$validator = $this->validatorFactory->make($data, $rules, $messages);
if ($validator->fails()) {
throw new ValidationException($validator);
}
验证规则字符串与 Laravel 几乎一致,如 required|string|min:6。此外还支持对象规则、条件规则等。
常用规则:
required:必填string、integer、array:类型min:6、max:20:长度/大小限制email:邮箱格式unique:table,column:数据库唯一性(需要数据库连接)confirmed:匹配{field}_confirmation字段
2. 异常处理流程
Swoole 环境下,PHP 的未捕获异常会直接导致 Worker 进程退出,甚至丢失响应。Hyperf 通过自己的异常处理器接管全局错误:
- 所有异常都会经过
Hyperf\ExceptionHandler\ExceptionHandlerDispatcher分发。 - 你可以在
config/autoload/exceptions.php中注册处理器,并按优先级排序。 - 一个处理器实现
Hyperf\ExceptionHandler\ExceptionHandler接口,包含handle()和isValid()方法。isValid()判断该处理器是否处理当前异常,handle()则构造并返回一个Response。
我们通常创建一个通用的 AppExceptionHandler 处理所有未被特定处理器捕获的异常,将错误信息 JSON 化,并记录日志。
3. 验证异常与业务异常的结合
Laravel 验证器会抛出 Hyperf\Validation\ValidationException,如果不处理,最终会落到通用异常处理器。我们可以创建一个专门的 ValidationExceptionHandler,将验证错误格式化为统一的 JSON 结构,例如:
{
"code": 422,
"message": "验证失败",
"errors": {
"username": ["用户名必填"],
"password": ["密码至少6位"]
}
}
三、实战:构建注册接口并加固(约 2.5 小时)
步骤 1:创建注册控制器与路由
新建 app/Controller/RegisterController.php(注意命名规范):
<?php
namespace App\Controller;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\Validation\Contract\ValidatorFactoryInterface;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Validation\ValidationException;
#[Controller(prefix: '/auth')]
class RegisterController extends AbstractController
{
/**
* 注入验证器工厂
*/
#[Inject]
protected ValidatorFactoryInterface $validatorFactory;
#[RequestMapping(path: 'register', methods: 'post')]
public function register()
{
$data = $this->request->all();
// 定义验证规则
$rules = [
'username' => 'required|string|min:3|max:20',
'email' => 'required|email',
'password' => 'required|string|min:6|confirmed', // confirmed 会自动校验 password_confirmation
];
$messages = [
'username.required' => '用户名必填',
'username.min' => '用户名至少3个字符',
'email.required' => '邮箱必填',
'email.email' => '邮箱格式不正确',
'password.required' => '密码必填',
'password.min' => '密码至少6位',
'password.confirmed'=> '两次密码输入不一致',
];
$validator = $this->validatorFactory->make($data, $rules, $messages);
if ($validator->fails()) {
throw new ValidationException($validator);
}
// 模拟注册逻辑:实际应保存数据库
// 这里我们直接返回成功,并记录日志
$validated = $validator->validated();
// 注意:密码不要明文存储,这里仅演示
// User::create($validated);
return [
'code' => 200,
'message' => '注册成功',
'data' => [
'username' => $validated['username'],
'email' => $validated['email'],
]
];
}
}
注意:我们使用了 #[Inject] 注解注入 ValidatorFactoryInterface,这是依赖注入的优雅方式(明天会专门深入学习)。控制器方法中,如果验证失败,我们主动抛出 ValidationException,这会触发我们接下来要编写的异常处理器。
步骤 2:添加路由
在 config/routes.php 中添加:
Router::post('/auth/register', [App\Controller\RegisterController::class, 'register']);
步骤 3:创建验证异常处理器
新建 app/Exception/Handler/ValidationExceptionHandler.php:
<?php
namespace App\Exception\Handler;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\Validation\ValidationException;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Psr\Http\Message\ResponseInterface;
use Throwable;
class ValidationExceptionHandler extends ExceptionHandler
{
public function handle(Throwable $throwable, ResponseInterface $response)
{
// 停止异常冒泡,不再继续传递给其他处理器
$this->stopPropagation();
/** @var ValidationException $throwable */
$body = json_encode([
'code' => 422,
'message' => '请求参数验证失败',
'errors' => $throwable->validator->errors()->messages(),
], JSON_UNESCAPED_UNICODE);
return $response->withStatus(422)
->withHeader('Content-Type', 'application/json')
->withBody(new SwooleStream($body));
}
public function isValid(Throwable $throwable): bool
{
return $throwable instanceof ValidationException;
}
}
步骤 4:创建通用异常处理器
新建 app/Exception/Handler/AppExceptionHandler.php:
<?php
namespace App\Exception\Handler;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Psr\Http\Message\ResponseInterface;
use Throwable;
use Psr\Log\LoggerInterface;
use Hyperf\Di\Annotation\Inject;
class AppExceptionHandler extends ExceptionHandler
{
#[Inject]
protected LoggerInterface $logger;
public function handle(Throwable $throwable, ResponseInterface $response)
{
// 记录错误日志
$this->logger->error(sprintf(
"%s: %s in %s:%d",
get_class($throwable),
$throwable->getMessage(),
$throwable->getFile(),
$throwable->getLine()
));
// 返回统一的 JSON 错误
$body = json_encode([
'code' => 500,
'message' => '服务器内部错误',
], JSON_UNESCAPED_UNICODE);
// 在开发环境可以暴露详细错误信息,生产环境关闭
if (env('APP_ENV') === 'dev') {
$body = json_encode([
'code' => 500,
'message' => $throwable->getMessage(),
'trace' => $throwable->getTraceAsString(),
], JSON_UNESCAPED_UNICODE);
}
return $response->withStatus(500)
->withHeader('Content-Type', 'application/json')
->withBody(new SwooleStream($body));
}
public function isValid(Throwable $throwable): bool
{
// 所有未被其他处理器捕获的异常都由这个处理器处理
return true;
}
}
步骤 5:注册异常处理器
打开 config/autoload/exceptions.php(如果不存在则创建),配置处理器顺序(优先级按数组顺序):
<?php
return [
'handler' => [
'http' => [
// 优先处理验证异常
App\Exception\Handler\ValidationExceptionHandler::class,
// 然后处理通用异常
App\Exception\Handler\AppExceptionHandler::class,
],
],
];
注意:exceptions.php 是 Hyperf 约定的配置文件名,框架自动加载。你可以查看 config/autoload 下的其他文件作为参考。
步骤 6:测试验证与异常
重启服务(热重启会自动进行,但如果没有使用 server:watch,需要手动重启)。
正常注册:
curl -X POST http://localhost:9501/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"Swoole","email":"swoole@php.net","password":"123456","password_confirmation":"123456"}'
预期返回:
{"code":200,"message":"注册成功","data":{"username":"Swoole","email":"swoole@php.net"}}
验证失败(缺少字段):
curl -X POST http://localhost:9501/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"S"}'
预期返回 422 及错误详情:
{
"code": 422,
"message": "请求参数验证失败",
"errors": {
"username": ["用户名至少3个字符"],
"email": ["邮箱必填"],
"password": ["密码必填"]
}
}
系统异常(模拟数据库错误):
我们可以故意在控制器中抛出一个 \Exception('数据库连接失败'),然后测试通用处理器。
// 在 register 方法中,return 之前添加:
throw new \Exception('数据库连接失败');
访问后,在开发环境返回 500 及详细错误,并可在 runtime/logs/ 下查看错误日志。
四、进阶实战:自定义验证规则(约 30 分钟)
Hyperf 允许你扩展验证器,创建自定义规则。例如我们添加一个规则:username 不能是保留字。
创建验证器扩展服务:新建 app/Validation/ValidatorExtension.php(或直接在 ConfigProvider 中注册,这里采用独立文件通过配置注册)。
但更简单的方式是使用闭包规则或扩展工厂,我们采用 Hyperf 推荐的扩展方法:在 config/autoload/dependencies.php 中通过工厂扩展。
另一种方式是直接使用 Validator::extend,但由于常驻内存,我们需要在启动时注册。可以在 App\Listener 中实现,但今天不涉及监听器。我们采用一种轻量方法:在控制器中动态添加规则:
$validator->addExtension('not_reserved', function ($attribute, $value, $parameters, $validator) {
$reserved = ['admin', 'root', 'system'];
return !in_array(strtolower($value), $reserved);
}, '该用户名是保留字');
// 然后在 rules 中可以使用 'not_reserved' 规则
$rules = [
'username' => 'required|string|min:3|max:20|not_reserved',
];
但这种方法每次请求都要添加,更优雅的做法是创建一个 验证器中间件 或 服务提供者。为了不偏离今天的主题,我们展示如何在 ValidatorFactory 上注册扩展,通过依赖注入扩展。
新建 app/Listener/ValidatorFactoryListener.php:
<?php
namespace App\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BootApplication;
use Hyperf\Validation\Contract\ValidatorFactoryInterface;
use Hyperf\Di\Annotation\Inject;
class RegisterValidatorRules implements ListenerInterface
{
#[Inject]
protected ValidatorFactoryInterface $validatorFactory;
public function listen(): array
{
return [
BootApplication::class,
];
}
public function process(object $event)
{
$this->validatorFactory->extend('not_reserved', function ($attribute, $value, $parameters, $validator) {
$reserved = ['admin', 'root', 'system'];
return !in_array(strtolower($value), $reserved);
}, ':attribute 是保留字');
}
}
由于时间关系,我们可以在学习监听器之后再来完善。今天你可以简单地在控制器内添加扩展来体验。
五、成果测试与总结(约 1 小时)
1. 完整测试清单
| 场景 | 测试数据 | 预期结果 |
|---|---|---|
| 正常注册 | 合法用户名、邮箱、密码 + 确认密码 | 200 成功 |
| 用户名过短 | username=S | 422,errors.username 含长度提示 |
| 邮箱格式错 | email=invalid | 422,errors.email 含格式提示 |
| 密码不一致 | password=123456, confirmation=654321 | 422,errors.password 含不一致提示 |
| 缺少多个字段 | 仅传部分字段 | 422,errors 列出所有缺失字段 |
| 未处理异常 | 控制器内抛异常 | 500,开发环境显示详细错误,日志记录 |
| 自定义规则 | username=admin(需实现扩展) | 422,errors.username 含保留字提示 |
2. 测试方法
使用 curl 或 Postman 逐个发送请求,观察 HTTP 状态码和 JSON 结构。对于异常情况,检查 runtime/logs/hyperf.log 是否有记录。
3. 思考题
- 异常处理器顺序:如果将
AppExceptionHandler放在ValidationExceptionHandler前面会发生什么?验证异常会被通用处理器捕获,因为通用处理器的isValid返回true,导致验证异常永远不会被特定处理器处理。这就是优先级的重要性。 - 业务异常:我们可以创建
BusinessException类,携带自定义错误码和消息,然后编写专用的BusinessExceptionHandler,实现统一的业务错误响应。 - 与 AOP 结合:我们可以在切面中抛出业务异常,异常处理器会正常捕获并返回 JSON,这完美符合微服务架构的错误处理范式。
六、今日作业与学习产出
- 提交代码:将
RegisterController、两个异常处理器、配置文件提交到 Git。 - 学习笔记:绘制从请求到验证器、异常处理器、响应返回的完整流程图,标注关键类。
- 实战拓展:
- 创建一个
BusinessException和对应的处理器,实现throw new BusinessException(1001, '用户不存在')在控制器中,验证响应格式。 - 为注册接口增加数据库唯一性验证(需要先学习数据库,可以提前查看文档),尝试
unique:users,username规则。
- 创建一个
- 思考:为什么在 Swoole 环境下,全局异常处理如此重要?如果异常没有捕获,Worker 进程会直接退出,导致服务中断。
通过今天的学习,你的 API 已经具备了生产级的健壮性:输入校验和错误响应都优雅且统一。明天我们将深入依赖注入容器,彻底明白 #[Inject] 背后的魔法,并将之前的知识融会贯通。
更多推荐
所有评论(0)