【基于 Swoole+Hyperf 的微服务实战】 第二周·周三:Hyperf 验证器与异常处理 今天主题是Hyperf 验证器与异常处理。在构建 API 时健壮的输入校验和统一的错误响应是专业性的体现。今天你将学会如何使用hyperf/validation组件为接口定义规则并编写全局异常处理器将任何错误都转化为美观的 JSON 响应不再暴露丑陋的调试信息。今日目标掌握 Hyperf 验证器的安装、配置与基本用法。能够为复杂的注册接口编写验证规则并自定义错误消息。理解 Hyperf 的异常处理流程创建自定义异常处理器。实现一个ApiExceptionHandler统一将所有异常格式化为 JSON 响应并区分业务异常与系统异常。结合验证器与异常处理打造一个健壮的注册 API并编写测试用例。一、环境准备约 20 分钟继续在hyperf-app项目中工作确保热重启已开启若未开启可执行php bin/hyperf.php server:watch。cdswoole-coursedocker-composeexecswoolebashcd/var/www/hyperf-app安装验证器组件Hyperf 的验证器基于 Laravel 的illuminate/validation需要额外安装composerrequire 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()){thrownewValidationException($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注意命名规范?phpnamespaceApp\Controller;useHyperf\HttpServer\Annotation\Controller;useHyperf\HttpServer\Annotation\RequestMapping;useHyperf\Validation\Contract\ValidatorFactoryInterface;useHyperf\Di\Annotation\Inject;useHyperf\Validation\ValidationException;#[Controller(prefix:/auth)]classRegisterControllerextendsAbstractController{/** * 注入验证器工厂 */#[Inject]protectedValidatorFactoryInterface$validatorFactory;#[RequestMapping(path:register,methods:post)]publicfunctionregister(){$data$this-request-all();// 定义验证规则$rules[usernamerequired|string|min:3|max:20,emailrequired|email,passwordrequired|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()){thrownewValidationException($validator);}// 模拟注册逻辑实际应保存数据库// 这里我们直接返回成功并记录日志$validated$validator-validated();// 注意密码不要明文存储这里仅演示// User::create($validated);return[code200,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?phpnamespaceApp\Exception\Handler;useHyperf\ExceptionHandler\ExceptionHandler;useHyperf\Validation\ValidationException;useHyperf\HttpMessage\Stream\SwooleStream;usePsr\Http\Message\ResponseInterface;useThrowable;classValidationExceptionHandlerextendsExceptionHandler{publicfunctionhandle(Throwable$throwable,ResponseInterface$response){// 停止异常冒泡不再继续传递给其他处理器$this-stopPropagation();/** var ValidationException $throwable */$bodyjson_encode([code422,message请求参数验证失败,errors$throwable-validator-errors()-messages(),],JSON_UNESCAPED_UNICODE);return$response-withStatus(422)-withHeader(Content-Type,application/json)-withBody(newSwooleStream($body));}publicfunctionisValid(Throwable$throwable):bool{return$throwableinstanceofValidationException;}}步骤 4创建通用异常处理器新建app/Exception/Handler/AppExceptionHandler.php?phpnamespaceApp\Exception\Handler;useHyperf\ExceptionHandler\ExceptionHandler;useHyperf\HttpMessage\Stream\SwooleStream;usePsr\Http\Message\ResponseInterface;useThrowable;usePsr\Log\LoggerInterface;useHyperf\Di\Annotation\Inject;classAppExceptionHandlerextendsExceptionHandler{#[Inject]protectedLoggerInterface$logger;publicfunctionhandle(Throwable$throwable,ResponseInterface$response){// 记录错误日志$this-logger-error(sprintf(%s: %s in %s:%d,get_class($throwable),$throwable-getMessage(),$throwable-getFile(),$throwable-getLine()));// 返回统一的 JSON 错误$bodyjson_encode([code500,message服务器内部错误,],JSON_UNESCAPED_UNICODE);// 在开发环境可以暴露详细错误信息生产环境关闭if(env(APP_ENV)dev){$bodyjson_encode([code500,message$throwable-getMessage(),trace$throwable-getTraceAsString(),],JSON_UNESCAPED_UNICODE);}return$response-withStatus(500)-withHeader(Content-Type,application/json)-withBody(newSwooleStream($body));}publicfunctionisValid(Throwable$throwable):bool{// 所有未被其他处理器捕获的异常都由这个处理器处理returntrue;}}步骤 5注册异常处理器打开config/autoload/exceptions.php如果不存在则创建配置处理器顺序优先级按数组顺序?phpreturn[handler[http[// 优先处理验证异常App\Exception\Handler\ValidationExceptionHandler::class,// 然后处理通用异常App\Exception\Handler\AppExceptionHandler::class,],],];注意exceptions.php是 Hyperf 约定的配置文件名框架自动加载。你可以查看config/autoload下的其他文件作为参考。步骤 6测试验证与异常重启服务热重启会自动进行但如果没有使用server:watch需要手动重启。正常注册curl-XPOST http://localhost:9501/auth/register\-HContent-Type: application/json\-d{username:Swoole,email:swoolephp.net,password:123456,password_confirmation:123456}预期返回{code:200,message:注册成功,data:{username:Swoole,email:swoolephp.net}}验证失败缺少字段curl-XPOST http://localhost:9501/auth/register\-HContent-Type: application/json\-d{username:S}预期返回 422 及错误详情{code:422,message:请求参数验证失败,errors:{username:[用户名至少3个字符],email:[邮箱必填],password:[密码必填]}}系统异常模拟数据库错误我们可以故意在控制器中抛出一个\Exception(数据库连接失败)然后测试通用处理器。// 在 register 方法中return 之前添加thrownew\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[usernamerequired|string|min:3|max:20|not_reserved,];但这种方法每次请求都要添加更优雅的做法是创建一个验证器中间件或服务提供者。为了不偏离今天的主题我们展示如何在ValidatorFactory上注册扩展通过依赖注入扩展。新建app/Listener/ValidatorFactoryListener.php?phpnamespaceApp\Listener;useHyperf\Event\Contract\ListenerInterface;useHyperf\Framework\Event\BootApplication;useHyperf\Validation\Contract\ValidatorFactoryInterface;useHyperf\Di\Annotation\Inject;classRegisterValidatorRulesimplementsListenerInterface{#[Inject]protectedValidatorFactoryInterface$validatorFactory;publicfunctionlisten():array{return[BootApplication::class,];}publicfunctionprocess(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 成功用户名过短usernameS422errors.username 含长度提示邮箱格式错emailinvalid422errors.email 含格式提示密码不一致password123456, confirmation654321422errors.password 含不一致提示缺少多个字段仅传部分字段422errors 列出所有缺失字段未处理异常控制器内抛异常500开发环境显示详细错误日志记录自定义规则usernameadmin需实现扩展422errors.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]背后的魔法并将之前的知识融会贯通。