ThinkPHP6/8 验证器批量验证BUG修复:数组错误信息处理方案
在ThinkPHP6和ThinkPHP8的开发过程中,当使用验证器的批量验证功能时,如果错误信息定义为数组格式,会遇到”Array to string conversion”的错误。本文将深入分析这个问题的原因,并提供多种解决方案。
问题描述 当我们在验证器中定义错误信息为数组格式,并开启批量验证时,会遇到以下错误:
1 Array to string conversion
问题复现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <?php namespace app \validate ;use think \Validate ;class User extends Validate { protected $rule = [ 'name' => 'require|max:25' , 'age' => 'number|between:1,120' , 'email' => 'email' , ]; protected $message = [ 'name.require' => ['code' => 1001 , 'msg' => '名称必须' ], 'name.max' => ['code' => 1002 , 'msg' => '名称最多不能超过25个字符' ], 'age.number' => ['code' => 1003 , 'msg' => '年龄必须是数字' ], 'age.between' => ['code' => 1004 , 'msg' => '年龄必须在1~120之间' ], 'email' => ['code' => 1005 , 'msg' => '邮箱格式错误' ], ]; protected $scene = [ 'add' => ['name' , 'age' , 'email' ], 'edit' => ['name' , 'age' ], ]; }
当开启批量验证时:
1 2 3 $validate = new \app\validate\User ();$result = $validate ->batch (true )->check ($data );
问题原因分析 源码分析 问题出现在 vendor/topthink/framework/src/think/exception/ValidateException.php 文件中:
1 2 $this ->message = is_array ($error ) ? implode (PHP_EOL, $error ) : $error ;
错误机制解析
正常情况 :当错误信息为字符串时,批量验证返回一维数组,implode() 可以正常处理
问题情况 :当错误信息为数组时,批量验证返回二维数组,implode() 无法直接处理二维数组
1 2 3 4 5 6 7 8 9 10 11 12 13 $errors = [ 'name.require' => '名称必须' , 'age.number' => '年龄必须是数字' ]; $errors = [ 'name.require' => ['code' => 1001 , 'msg' => '名称必须' ], 'age.number' => ['code' => 1003 , 'msg' => '年龄必须是数字' ] ];
解决方案 方案一:修改核心文件(不推荐) 虽然可以直接修改框架核心文件,但不推荐这种做法,因为框架更新时会覆盖修改。
1 2 3 4 5 6 $this ->message = is_array ($error ) ? implode (PHP_EOL, $error ) : $error ;$this ->message = is_array ($error ) ? json_encode ($error , JSON_UNESCAPED_UNICODE) : $error ;
方案二:自定义验证异常类(推荐) 创建自定义的验证异常处理类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 <?php namespace app \exception ;use think \exception \ValidateException as BaseValidateException ;class ValidateException extends BaseValidateException { public function __construct ($error ) { if (is_array ($error )) { $this ->message = $this ->formatArrayError ($error ); } else { $this ->message = $error ; } $this ->code = 0 ; } protected function formatArrayError (array $errors ): string { $formatted = []; foreach ($errors as $field => $error ) { if (is_array ($error )) { if (isset ($error ['msg' ])) { $formatted [] = $error ['msg' ]; } else { $formatted [] = json_encode ($error , JSON_UNESCAPED_UNICODE); } } else { $formatted [] = $error ; } } return implode (PHP_EOL, $formatted ); } public function getRawError ( ) { return $this ->error ?? $this ->message; } }
方案三:自定义验证器基类 创建自定义验证器基类,重写验证方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 <?php namespace app \common ;use think \Validate ;use app \exception \ValidateException ;abstract class BaseValidate extends Validate { protected $batchValidate = false ; public function batch (bool $batch = true ) { $this ->batchValidate = $batch ; return parent ::batch ($batch ); } public function check (array $data , array $rules = [], array $message = [], string $scene = '' ): bool { try { return parent ::check ($data , $rules , $message , $scene ); } catch (\think\exception\ValidateException $e ) { if ($this ->batchValidate && $this ->hasArrayError ($this ->error)) { throw new ValidateException ($this ->error); } throw $e ; } } protected function hasArrayError ($error ): bool { if (!is_array ($error )) { return false ; } foreach ($error as $item ) { if (is_array ($item )) { return true ; } } return false ; } public function getFormattedError ( ): array { if (!is_array ($this ->error)) { return ['message' => $this ->error]; } $formatted = []; foreach ($this ->error as $field => $error ) { if (is_array ($error )) { $formatted [$field ] = $error ; } else { $formatted [$field ] = ['message' => $error ]; } } return $formatted ; } }
方案四:使用字符串错误信息(简单方案) 最简单的解决方案是避免使用数组格式的错误信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <?php namespace app \validate ;use app \common \BaseValidate ;class User extends BaseValidate { protected $rule = [ 'name' => 'require|max:25' , 'age' => 'number|between:1,120' , 'email' => 'email' , ]; protected $message = [ 'name.require' => '名称必须' , 'name.max' => '名称最多不能超过25个字符' , 'age.number' => '年龄必须是数字' , 'age.between' => '年龄必须在1~120之间' , 'email' => '邮箱格式错误' , ]; protected $errorCodes = [ 'name.require' => 1001 , 'name.max' => 1002 , 'age.number' => 1003 , 'age.between' => 1004 , 'email' => 1005 , ]; public function getErrorCode (string $rule ): int { return $this ->errorCodes[$rule ] ?? 0 ; } }
高级解决方案 统一错误处理中间件 创建统一的错误处理中间件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 <?php namespace app \middleware ;use think \Request ;use think \Response ;use think \exception \Handle ;use app \exception \ValidateException ;class ErrorHandler { public function handle (Request $request , \Closure $next ): Response { try { return $next ($request ); } catch (ValidateException $e ) { return $this ->handleValidateException ($e ); } catch (\think\exception\ValidateException $e ) { return $this ->handleThinkValidateException ($e ); } } protected function handleValidateException (ValidateException $e ): Response { $error = $e ->getRawError (); if (is_array ($error )) { return json ([ 'code' => 422 , 'message' => '数据验证失败' , 'errors' => $error ], 422 ); } return json ([ 'code' => 422 , 'message' => $error ], 422 ); } protected function handleThinkValidateException (\think\exception\ValidateException $e ): Response { return json ([ 'code' => 422 , 'message' => $e ->getMessage () ], 422 ); } }
验证器工厂类 创建验证器工厂类,统一处理验证逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 <?php namespace app \service ;use think \facade \Validate ;class ValidatorFactory { public static function create (array $rules , array $messages = [], bool $batch = false ): \think \Validate { $validator = Validate ::rule ($rules ); if (!empty ($messages )) { $processedMessages = self ::processMessages ($messages ); $validator ->message ($processedMessages ); } if ($batch ) { $validator ->batch (true ); } return $validator ; } protected static function processMessages (array $messages ): array { $processed = []; foreach ($messages as $key => $message ) { if (is_array ($message )) { $processed [$key ] = $message ['msg' ] ?? json_encode ($message , JSON_UNESCAPED_UNICODE); } else { $processed [$key ] = $message ; } } return $processed ; } public static function validate (array $data , array $rules , array $messages = [], bool $batch = false ): array { $validator = self ::create ($rules , $messages , $batch ); if ($validator ->check ($data )) { return ['success' => true , 'data' => $data ]; } $errors = $validator ->getError (); if ($batch && !empty ($messages )) { $formattedErrors = self ::formatBatchErrors ($errors , $messages ); return ['success' => false , 'errors' => $formattedErrors ]; } return ['success' => false , 'errors' => $errors ]; } protected static function formatBatchErrors (array $errors , array $originalMessages ): array { $formatted = []; foreach ($errors as $field => $error ) { foreach ($originalMessages as $key => $message ) { if (strpos ($key , $field ) === 0 && is_array ($message )) { $formatted [$field ] = $message ; break ; } } if (!isset ($formatted [$field ])) { $formatted [$field ] = ['message' => $error ]; } } return $formatted ; } }
使用示例 控制器中的使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 <?php namespace app \api \controller ;use app \BaseController ;use app \service \ValidatorFactory ;use think \Request ;use think \Response ;class User extends BaseController { public function create (Request $request ): Response { $data = $request ->post (); $rules = [ 'name' => 'require|max:25' , 'age' => 'number|between:1,120' , 'email' => 'email' , ]; $messages = [ 'name.require' => ['code' => 1001 , 'msg' => '名称必须' ], 'name.max' => ['code' => 1002 , 'msg' => '名称最多不能超过25个字符' ], 'age.number' => ['code' => 1003 , 'msg' => '年龄必须是数字' ], 'age.between' => ['code' => 1004 , 'msg' => '年龄必须在1~120之间' ], 'email' => ['code' => 1005 , 'msg' => '邮箱格式错误' ], ]; $result = ValidatorFactory ::validate ($data , $rules , $messages , true ); if (!$result ['success' ]) { return json ([ 'code' => 422 , 'message' => '数据验证失败' , 'errors' => $result ['errors' ] ], 422 ); } return json ([ 'code' => 200 , 'message' => '用户创建成功' , 'data' => $result ['data' ] ]); } }
API响应格式 成功响应:
1 2 3 4 5 6 7 8 9 { "code" : 200 , "message" : "用户创建成功" , "data" : { "name" : "张三" , "age" : 25 , "email" : "zhangsan@example.com" } }
验证失败响应:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "code" : 422 , "message" : "数据验证失败" , "errors" : { "name" : { "code" : 1001 , "msg" : "名称必须" } , "age" : { "code" : 1003 , "msg" : "年龄必须是数字" } } }
最佳实践建议 1. 统一错误码管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <?php namespace app \constant ;class ErrorCode { const USER_NAME_REQUIRED = 1001 ; const USER_NAME_TOO_LONG = 1002 ; const USER_AGE_INVALID = 1003 ; const USER_AGE_OUT_OF_RANGE = 1004 ; const USER_EMAIL_INVALID = 1005 ; const MESSAGES = [ self ::USER_NAME_REQUIRED => '用户名不能为空' , self ::USER_NAME_TOO_LONG => '用户名长度不能超过25个字符' , self ::USER_AGE_INVALID => '年龄必须是数字' , self ::USER_AGE_OUT_OF_RANGE => '年龄必须在1-120之间' , self ::USER_EMAIL_INVALID => '邮箱格式不正确' , ]; public static function getMessage (int $code ): string { return self ::MESSAGES [$code ] ?? '未知错误' ; } }
2. 验证器配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php return [ 'batch' => true , 'error_format' => 'array' , 'default_error_code' => 400 , 'validators' => [ 'user' => \app\validate\User ::class , 'order' => \app\validate\Order ::class , ], ];
3. 国际化支持 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 return [ 'user.name.require' => '用户名不能为空' , 'user.name.max' => '用户名长度不能超过:max个字符' , 'user.age.number' => '年龄必须是数字' , 'user.age.between' => '年龄必须在:min-:max之间' , 'user.email.email' => '邮箱格式不正确' , ]; return [ 'user.name.require' => 'Name is required' , 'user.name.max' => 'Name cannot exceed :max characters' , 'user.age.number' => 'Age must be a number' , 'user.age.between' => 'Age must be between :min and :max' , 'user.email.email' => 'Invalid email format' , ];
总结 ThinkPHP6/8验证器批量验证的数组错误信息BUG主要是由于框架在处理二维数组时使用了不当的字符串拼接方法。本文提供了多种解决方案:
自定义验证异常类 :推荐方案,不修改框架核心代码
自定义验证器基类 :提供更好的封装和扩展性
验证器工厂类 :统一验证逻辑,支持复杂的错误信息格式
字符串错误信息 :最简单的解决方案
在实际项目中,建议根据具体需求选择合适的解决方案,并结合统一的错误码管理和国际化支持,构建完善的数据验证体系。