支持Laravel.io的持续开发 →

理解通配符符号在验证规则中的工作原理

2022年6月14日 阅读时间:5分钟

Laravel中的数组验证功能非常强大,但在处理多维度数组和通配符表达式时,有时它可能不会按照我们预期的方式工作。让我们通过一个实际的例子深入了解它的工作机制,并在此过程中加深对这个功能特性的理解。

首先,让我们设置一个现实世界的例子。我们有一个简单的表单来收集团队的午餐偏好。我们将统计最受欢迎的选项,并以此为午餐的菜单。为了防止“舞弊”行为,我们希望确保每个团队成员不会对同一项选择重复投票。

以下是一组示例提交,以及一组看起来非常直接的验证规则

<?php

$data = [
    'team_meal_preferences' => [
        [ 'pizza', 'sushi', 'tacos' ],
        [ 'tacos', 'pizza' ],
        [ 'waffles', 'sushi' ],
    ],
];

$rules = [
    'team_meal_preferences' => [
        'array',
    ],
    'team_meal_preferences.*' => [
        'array',
        'min:2',
        'max:3',
    ],
    'team_meal_preferences.*.*' => [
        'string',
        'distinct',
    ],
];

阅读这些规则时,我们发现每个团队成员必须至少提供2个午餐选择,但不能超过3个。我们尝试确保他们不会选择相同的选项重复 voting3,但实际上这里的情况并不完全符合我们的预期。尽管没有团队成员违反distinct规则,这段$data仍然会失败验证。这里是会返回的准确错误信息。

{
  "team_meal_preferences.0.0":["The team_meal_preferences.0.0 field has a duplicate value."],
  "team_meal_preferences.0.1":["The team_meal_preferences.0.1 field has a duplicate value."],
  "team_meal_preferences.0.2":["The team_meal_preferences.0.2 field has a duplicate value."],
  "team_meal_preferences.1.0":["The team_meal_preferences.1.0 field has a duplicate value."],
  "team_meal_preferences.1.1":["The team_meal_preferences.1.1 field has a duplicate value."],
  "team_meal_preferences.2.1":["The team_meal_preferences.2.1 field has a duplicate value."],
}

校验器失败的原因在于它在评估 `distinct` 规则时,将每个团队成员的选择一起考虑。为什么会出现这种情况?让我们深入探究一下,看看Laravel验证中的通配符表示法是如何评估的。

当构造 `Validator` 实例时,它将所有规则设置为一个实例的属性。如果我们跟踪一下逻辑,会看到一个名为 `addRules` 的方法,该方法包含了一行代码和一个非常有用的注释,这对我们特别感兴趣。

<?php

// The primary purpose of this parser is to expand any "*" rules to all
// of the explicit rules needed for the given data. For example the rule
// names.* would get expanded to names.0, names.1, etc. for this data.
$response = (new ValidationRuleParser($this->data))
                    ->explode(ValidationRuleParser::filterConditionalRules($rules, $this->data));

正如注释所解释的,这个规则解析器会将任何通配符规则根据我们数据有效负载的精确形状,分解成许多个独立的规则。在我们的例子中,它将我们的distinct规则转换成这样:

// initial rule
team_meal_preferences.*.* = ['distinct']

// exploded rules after parsing
team_meal_preferences.0.0 = ['distinct']
team_meal_preferences.0.1 = ['distinct']
team_meal_preferences.0.2 = ['distinct']
team_meal_preferences.1.0 = ['distinct']
team_meal_preferences.1.1 = ['distinct']
team_meal_preferences.2.0 = ['distinct']
team_meal_preferences.2.1 = ['distinct']

需要注意的是,这些“分解”的规则与我们的数据相关。如果我们只有一个默默无闻的团队成员提交他们的3个餐点偏好,那么它只会生成3个分解规则:`0.0`,`0.1`,`0.2`。(这个团队成员在午餐时可能很孤独,但他们一定会得到他们确切的餐点选择!)

除此之外,它还使用 `Arr::dot` 辅助函数对我们的待验证数据进行转换。

// initial data
'team_meal_preferences' => [
    [ 'pizza', 'sushi', 'tacos' ],
    [ 'tacos', 'pizza' ],
    [ 'waffles', 'sushi' ],
]

// transformed data while parsing rules
team_meal_preferences.0.0 = "pizza"
team_meal_preferences.0.1 = "sushi"
team_meal_preferences.0.2 = "tacos"
team_meal_preferences.1.0 = "tacos"
team_meal_preferences.1.1 = "pizza"
team_meal_preferences.2.0 = "waffles"
team_meal_preferences.2.1 = "sushi"

这种数据转换在验证器中通过属性 `implicitAttributes` 进行跟踪。它将有效数据中的原始键映射到单个转换后的值。在我们在实际的验证逻辑中前进时,这是一个需要记住的重要点。

仅从上文来看,我们可能已经对验证逻辑失败的原因有了更深的理解,但让我们解开最后一块拼图,让它更加清晰。

负责实现 `distinct` 规则的方法被恰当地命名为 `validateDistinct`。它首先检查我们正在验证的属性是否由于通配符而分解。请记住,这是因为跟踪在验证器的 `implicitAttribute` 属性中的。在我们的情况下,我们看到 `team_meal_preferences.0.0` 实际上是主属性 `team_meal_preferences`。然后使用这个主属性名称来收集比较是否唯一的值集合。由于之前的通配符扩展,它拉入整个转换后的数据值集合用于 `distinct` 测试。

太好了!我们了解了为什么它没有按预期工作。解决方案是什么?

使用我们现有的数据和代码示例,我们可以这样修复它

<?php
// $data definition omitted to keep it short

$rules = [
    'team_meal_preferences' => [
        'array',
    ],
    'team_meal_preferences.*' => [
        'array',
        'min:2',
        'max:3',
    ],
    // Instead of using a nested wildcard, we could limit it to one level
    'team_meal_preferences.0.*' => [
        'string',
        'distinct',
    ],
    'team_meal_preferences.1.*' => [
        'string',
        'distinct',
    ],
    'team_meal_preferences.2.*' => [
        'string',
        'distinct',
    ],
];

正如你可能会想的,这并不实用。如果明天有更多的人或更少的人对午餐偏好进行投票,怎么办?为了解决这个问题,我们可以使用一点点我们自己的动态规则生成,就像验证器在内部所做的那样,但调整为适应我们想要的 `distinct` 规则的嵌套级别。

<?php
// $data definition omitted to keep it short

$teamPreferenceRules = [];
foreach ($data['team_meal_preferences'] as $key => $val)
{
    $teamPreferenceRules["team_meal_preferences.{$key}.*"] = [
        'string',
        'distinct',
    ];
}

$rules = [
    'team_meal_preferences' => [
        'array',
    ],
    'team_meal_preferences.*' => [
        'array',
        'min:2',
        'max:3',
    ],
    ...$teamPreferenceRules,
];

Laravel 9还有一个新的 `Rule::forEach 方法,但它并没有改变通配符扩展的方式,所以我认为它对于解决这个特定问题(多维数组与 `distinct` 验证规则)并不适用。

你对这个问题有更好的解决方案吗?告诉我!

上次更新1年前。

用户*driesvints*喜欢这篇文章

1
喜欢这篇文章吗? 告诉作者并给他们一个 applause!
joelclermont (Joel Clermont) 我帮助Laravel SaaS团队交付更好的产品。Podcast在show.nocompromises.io. masteringlaravel.io 的作者

你可能还喜欢这些文章

如何使用Larastan让您从0到9进阶Laravel应用

在您的Laravel应用执行之前发现缺陷是可能的,归功于Larastan,它是一个...

阅读文章

无需特性标准化API响应

我发现大多数用于API响应的库都是使用特性实现,...

阅读文章

在您的Laravel项目中通过Discord通知收集反馈

如何在Laravel项目中创建反馈模块并在收到消息时接收Discord通知...

阅读文章

感谢这些 了不起的公司 对我们的支持

您的标志在这里?

Laravel.io

问题解决、知识共享和社区建设的Laravel门户。

© 2024 Laravel.io - 版权所有。