ThinkPHP6 验证器兼容数组

在一些场景下我们会对数组进行验证。如果你之前使用过 Laravel 可能这块已经很熟悉了的,但 ThinkPHP6 现在是不支持的。

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
public function index ()
{
$data = [
'list' => [
[
'email' => 'hong@hongfs.cn',
],
[
'email1' => 'hong@hongfs.cn',
'value' => 'https://www.hongfs.cn/',
],
],
];

try {
$this->validate($data, [
'list' => 'require|array',
'list.*.email' => 'require|email',
'list.1.value' => 'require|url',
]);
}
catch (\think\exception\ValidateException $v) {
return $v->getMessage();
}

return '验证成功';
}

运行后我们会得到 list.*.email不能为空。通过追踪函数的执行,执行开始验证是位于 vendor/topthink/framework/src/think/Validate.php|check

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
public function check(array $data, array $rules = []): bool
{
$this->error = [];

foreach ($rules as $key => $rule) {
if (strpos($key, '|')) {
// 字段|描述 用于指定属性名称
[$key, $title] = explode('|', $key);
} else {
$title = $this->field[$key] ?? $key;
}

// 获取数据 支持二维数组
$value = $this->getDataValue($data, $key);

// 字段验证
if ($rule instanceof Closure) {
$result = call_user_func_array($rule, [$value, $data]);
} elseif ($rule instanceof ValidateRule) {
// 验证因子
$result = $this->checkItem($key, $value, $rule->getRule(), $data, $rule->getTitle() ?: $title, $rule->getMsg());
} else {
$result = $this->checkItem($key, $value, $rule, $data, $title);
}
}

return true;
}

通过上面的删减后的代码,可以了解到验证条件会通过 foreach 进行单独处理,然后获取这个验证条件需要的数据($this->getDataValue)。打印 $value 的值,会发现我们验证不通过的值返回是 null

最上面的代码其实也明示我们要兼容的验证规则了。.*. 代表这个数组所有条数都需要验证,.1. 代表我们要验证数组中索引为 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
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
/**
* 获取数据值
* @access protected
* @param array $data 数据
* @param string $key 数据标识 支持二维
* @return mixed
*/
protected function getDataValue(array $data, $key)
{
if (is_numeric($key)) {
$value = $key;
} elseif (is_string($key) && substr_count($key, '.') === 2) {
// 支持数组获取数据

// 获取第一个 . 的位置
$index = strpos($key, '.');

// 获取完整的数组数据
$list = $this->getDataValue($data, substr($key, 0, $index));

$value = [];

if(!is_array($list)) {
// 类型不是数组
} elseif (strpos($key, '.*.') !== false) {
// 数组需要提取
$_key = substr($key, strrpos($key, '.') + 1);

foreach ($list as $item_key => $item_value) {
// 如果不存在该键值也需要赋值一个 null 以确保数据的完整性
$value[$item_key] = $item_value[$_key] ?? null;
}

unset($_key, $item_key, $item_value);
} elseif (preg_match_all('/.\d./', $key)) {
// 提取单个
$_i = (int) explode('.', $key)[1];

$_key = substr($key, strrpos($key, '.') + 1);

$item = $list[$_i] ?? [];

$value = $item[$_key] ?? null;

unset($item, $_key, $_i);
} else {
}
} elseif (is_string($key) && strpos($key, '.')) {
// 支持多维数组验证
foreach (explode('.', $key) as $key) {
if (!isset($data[$key])) {
$value = null;
break;
}
$value = $data = $data[$key];
}
} else {
$value = $data[$key] ?? null;
}

return $value;
}

当验证条件存在 .*. 时,$this->getDataValue 返回的是一个一维数组,我们需要让 check 进行兼容处理。

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
public function check(array $data, array $rules = []): bool
{
foreach ($rules as $key => $rule) {
// 获取数据 支持二维数组
$value = $this->getDataValue($data, $key);

// 字段验证
if ($rule instanceof Closure) {
$result = call_user_func_array($rule, [$value, $data]);
} elseif ($rule instanceof ValidateRule) {
// 验证因子
$result = $this->checkItem($key, $value, $rule->getRule(), $data, $rule->getTitle() ?: $title, $rule->getMsg());
} elseif (strpos($key, '.*.') !== false) {
// 验证数组

foreach ($value as $item_index => $item_value) {
$result = $this->checkItem($key, $item_value, $rule, $data, $title);

if(true !== $result) {
// 没有返回true 则表示验证失败,结束数组的验证
break;
}
}
} else {
$result = $this->checkItem($key, $value, $rule, $data, $title);
}
}
}

这时如果重新执行代码,就能收到 验证成功

为了封装验证键值有问题,我们可能需要在循环的开始阻止一些非法的内容。

1
2
3
4
5
6
7
8
public function check(array $data, array $rules = []): bool
{
foreach ($rules as $key => $rule) {
if(strpos($key, '.*.') === 0) {
throw new \Exception('验证规则异常');
}
}
}
往上