ThinkPHP6/8 验证器批量验证BUG修复:数组错误信息处理方案
Orion K Lv6

在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;

错误机制解析

  1. 正常情况:当错误信息为字符串时,批量验证返回一维数组,implode() 可以正常处理
  2. 问题情况:当错误信息为数组时,批量验证返回二维数组,implode() 无法直接处理二维数组
1
2
3
4
5
6
7
8
9
10
11
12
13
// 正常情况(字符串错误信息)
$errors = [
'name.require' => '名称必须',
'age.number' => '年龄必须是数字'
];
// implode(PHP_EOL, $errors) 正常工作

// 问题情况(数组错误信息)
$errors = [
'name.require' => ['code' => 1001, 'msg' => '名称必须'],
'age.number' => ['code' => 1003, 'msg' => '年龄必须是数字']
];
// implode(PHP_EOL, $errors) 报错:Array to string conversion

解决方案

方案一:修改核心文件(不推荐)

虽然可以直接修改框架核心文件,但不推荐这种做法,因为框架更新时会覆盖修改。

1
2
3
4
5
6
// vendor/topthink/framework/src/think/exception/ValidateException.php
// 将原来的代码:
$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
{
/**
* 构造函数
* @param array|string $error 错误信息
*/
public function __construct($error)
{
if (is_array($error)) {
// 处理二维数组错误信息
$this->message = $this->formatArrayError($error);
} else {
$this->message = $error;
}

$this->code = 0;
}

/**
* 格式化数组错误信息
* @param array $errors 错误数组
* @return string 格式化后的错误信息
*/
protected function formatArrayError(array $errors): string
{
$formatted = [];

foreach ($errors as $field => $error) {
if (is_array($error)) {
// 如果错误信息是数组,提取msg字段或转为JSON
if (isset($error['msg'])) {
$formatted[] = $error['msg'];
} else {
$formatted[] = json_encode($error, JSON_UNESCAPED_UNICODE);
}
} else {
$formatted[] = $error;
}
}

return implode(PHP_EOL, $formatted);
}

/**
* 获取原始错误数据
* @return array|string
*/
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
{
/**
* 批量验证标识
* @var bool
*/
protected $batchValidate = false;

/**
* 设置批量验证
* @param bool $batch 是否批量验证
* @return $this
*/
public function batch(bool $batch = true)
{
$this->batchValidate = $batch;
return parent::batch($batch);
}

/**
* 验证数据
* @param array $data 数据
* @param array $rules 验证规则
* @param array $message 错误信息
* @param string $scene 验证场景
* @return bool
* @throws ValidateException
*/
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;
}
}

/**
* 检查是否包含数组错误信息
* @param mixed $error 错误信息
* @return bool
*/
protected function hasArrayError($error): bool
{
if (!is_array($error)) {
return false;
}

foreach ($error as $item) {
if (is_array($item)) {
return true;
}
}

return false;
}

/**
* 获取格式化的错误信息
* @return array
*/
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,
];

/**
* 获取错误代码
* @param string $rule 规则名称
* @return int
*/
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
{
/**
* 处理请求
* @param Request $request
* @param \Closure $next
* @return Response
*/
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);
}
}

/**
* 处理自定义验证异常
* @param ValidateException $e
* @return Response
*/
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);
}

/**
* 处理框架验证异常
* @param \think\exception\ValidateException $e
* @return Response
*/
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
{
/**
* 创建验证器实例
* @param array $rules 验证规则
* @param array $messages 错误信息
* @param bool $batch 是否批量验证
* @return \think\Validate
*/
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;
}

/**
* 处理错误信息
* @param array $messages 原始错误信息
* @return array 处理后的错误信息
*/
protected static function processMessages(array $messages): array
{
$processed = [];

foreach ($messages as $key => $message) {
if (is_array($message)) {
// 如果是数组,提取msg字段或转为字符串
$processed[$key] = $message['msg'] ?? json_encode($message, JSON_UNESCAPED_UNICODE);
} else {
$processed[$key] = $message;
}
}

return $processed;
}

/**
* 验证数据并返回格式化结果
* @param array $data 待验证数据
* @param array $rules 验证规则
* @param array $messages 错误信息
* @param bool $batch 是否批量验证
* @return array 验证结果
*/
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];
}

/**
* 格式化批量验证错误
* @param array $errors 验证错误
* @param array $originalMessages 原始错误信息
* @return array 格式化后的错误
*/
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
{
/**
* 创建用户
* @param Request $request
* @return Response
*/
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 => '邮箱格式不正确',
];

/**
* 获取错误信息
* @param int $code 错误码
* @return string 错误信息
*/
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
// config/validate.php
return [
// 是否开启批量验证
'batch' => true,

// 错误信息格式
'error_format' => 'array', // array | string

// 默认错误码
'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
// lang/zh-cn/validate.php
return [
'user.name.require' => '用户名不能为空',
'user.name.max' => '用户名长度不能超过:max个字符',
'user.age.number' => '年龄必须是数字',
'user.age.between' => '年龄必须在:min-:max之间',
'user.email.email' => '邮箱格式不正确',
];

// lang/en-us/validate.php
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主要是由于框架在处理二维数组时使用了不当的字符串拼接方法。本文提供了多种解决方案:

  1. 自定义验证异常类:推荐方案,不修改框架核心代码
  2. 自定义验证器基类:提供更好的封装和扩展性
  3. 验证器工厂类:统一验证逻辑,支持复杂的错误信息格式
  4. 字符串错误信息:最简单的解决方案

在实际项目中,建议根据具体需求选择合适的解决方案,并结合统一的错误码管理和国际化支持,构建完善的数据验证体系。

本站由 提供部署服务