【问题标题】:Laravel Validation: only allow known properties/attributes, otherwise fail validationLaravel 验证:只允许已知的属性/属性,否则验证失败
【发布时间】:2019-08-25 11:40:06
【问题描述】:

我们正在构建一个需要精度的 api 端点。我们希望对 POST/PUT 到服务器的参数强制执行严格验证。

如果 api 用户发送不支持的 key=value 对(例如,我们允许参数 [first_name, last_name] 并且用户包含不受支持的参数 [country]),我们希望验证失败。

已尝试构建一个名为allowed_attributes(用作allowed_attributes:attr1,attr2,...)的自定义验证器,但要使其在$validationRules 数组中可用,它必须应用于嵌套/子属性列表的父级(...因为否则我们的自定义验证器无法访问正在验证的属性)。

Validator::extend('allowed_attributes', 'App\Validators\AllowedAttributesValidator@validate');

这给其他验证器带来了问题,我们不得不预测这个父/子结构和围绕它的代码,包括额外的验证后清理错误键和错误消息字符串。

tl;dr:非常脏,不是一个干净的实现。

$validationRules = [
  'parent' => 'allowed_attributes:first_name,last_name',
  'parent.first_name' => 'required|string|max:40',
  'parent.last_name' => 'required|string|max:40'
];

$isValid = Validator::make(['parent' => $request], $validationRules);

var_dump("Validation results: " . ($isValid ? "passed" : "failed"));

关于如何在 laravel 中更干净地完成此操作,而不需要使用父/子关系来访问所有 $request 属性列表(在自定义验证器中)的任何想法/建议?

【问题讨论】:

    标签: php laravel validation


    【解决方案1】:

    它应该适用于使用此自定义验证器的简单键/值对:

    Validator::extendImplicit('allowed_attributes', function ($attribute, $value, $parameters, $validator) {
        // If the attribute to validate request top level
        if (strpos($attribute, '.') === false) {
            return in_array($attribute, $parameters);
        }
    
        // If the attribute under validation is an array
        if (is_array($value)) {
            return empty(array_diff_key($value, array_flip($parameters)));
        }
    
        // If the attribute under validation is an object
        foreach ($parameters as $parameter) {
            if (substr_compare($attribute, $parameter, -strlen($parameter)) === 0) {
                return true;
            }
        }
    
        return false;
    });
    

    验证器逻辑非常简单:

    • 如果$attribute 不包含.,我们正在处理顶级参数,我们只需要检查它是否存在于我们传递给规则的allowed_attributes 列表中。
    • 如果$attribute 的值是一个数组,我们将输入键与allowed_attributes 列表进行比较,并检查是否还有任何属性键。如果是这样,我们的请求有一个我们没有预料到的额外密钥,所以我们返回false
    • 否则$attribute 的值是一个对象,我们必须检查我们期望的每个参数(同样,allowed_attributes 列表)是否是当前属性的最后一段(因为 laravel 给了我们完整的点符号$attribute 中的属性)。

    这里的关键是将它应用到验证规则应该是这样的(注意第一个验证规则):

    $validationRules = [
      'parent.*' => 'allowed_attributes:first_name,last_name',
      'parent.first_name' => 'required|string|max:40',
      'parent.last_name' => 'required|string|max:40'
    ];
    

    parent.* 规则会将自定义验证器应用于“父”对象的每个键。

    回答你的问题

    只是不要将您的请求包装在对象中,而是使用与上述相同的概念并将allowed_attributes 规则与* 一起应用:

    $validationRules = [
      '*' => 'allowed_attributes:first_name,last_name',
      'first_name' => 'required|string|max:40',
      'last_name' => 'required|string|max:40'
    ];
    

    这会将规则应用于所有当前顶级输入请求字段。


    注意:请记住,laravel 验证受规则顺序的影响,因为它们被放入规则数组中。 例如,将parent.* 规则移到底部将触发parent.first_nameparent.last_name 上的该规则;相反,将其作为第一条规则不会触发first_namelast_name 的验证。

    这意味着您最终可以从allowed_attributes 规则的参数列表中删除具有进一步验证逻辑的属性。

    例如,如果您只想要求 first_namelast_name 并禁止 parent 对象中的任何其他字段,则可以使用以下规则:

    $validationRules = [
      // This will be triggered for all the request fields except first_name and last_name
      'parent.*' => 'allowed_attributes', 
      'parent.first_name' => 'required|string|max:40',
      'parent.last_name' => 'required|string|max:40'
    ];
    

    但是,以下不会按预期工作:

    $validationRules = [
      'parent.first_name' => 'required|string|max:40',
      'parent.last_name' => 'required|string|max:40',
      // This, instead would be triggered on all fields, also on first_name and last_name
      // If you put this rule as last, you MUST specify the allowed fields.
      'parent.*' => 'allowed_attributes', 
    ];
    

    数组小问题

    据我所知,根据 Laravel 的验证逻辑,如果您要验证一个对象数组,这个自定义验证器会起作用,但是您会得到的错误消息是数组项上的通用信息,而不是键上的不允许的数组项。

    例如,您允许在请求中包含一个 products 字段,每个字段都有一个 id:

    $validationRules = [
      'products.*' => 'allowed_attributes:id',
    ];
    

    如果您验证这样的请求:

    {
        "products": [{
            "id": 3
        }, {
            "id": 17,
            "price": 3.49
        }]
    }
    

    您将在产品 2 上收到错误,但您无法确定是哪个字段导致了问题!

    【讨论】:

    • 谢谢,我会试试这个并更新你。看起来与我们实现的主要区别在于使用Validator::extendImplicit。另外,正确,来自数组的错误消息格式不正确;我们对生成的错误消息进行了修改以提高含义,但仍然不理想。无法找到在我们的自定义错误消息中显示被拒绝的用户输入值的方法,因此必须对验证器进行硬编码以使用 $validator->errors()->add(...) 推送附加消息,这意味着在验证失败时从自定义验证器触发两条错误消息。跨度>
    • 也许作为最后一个资源,您可以扩展默认验证器类并覆盖验证逻辑以满足您的需求。然后将其绑定在应用容器中以覆盖默认的。
    • 嗯,AllowedAttributes 验证器存在问题,无法检测到输入数据中包含空字符串属性。例如。 ["first_name" => "Name", "" => "Value"] 不会通过验证检查。我们在 laravel 5.3 上,所以可能在较新的版本中对其进行了修补......?
    • 我会调查一下并告诉你。我们还可以在 stackoverflow 上开始聊天以处理所有情况。
    【解决方案2】:

    我更喜欢发布一个新答案,因为该方法与以前的方法不同并且更简洁。所以我宁愿将这两种方法分开,而不是在同一个答案中混合在一起。

    更好的问题处理

    自从我上次回答以来,在深入研究了 Validation 的命名空间的源代码后,我发现最简单的方法是扩展 Validator 类以重新实现 passes() 函数来检查您需要什么。

    此实现的好处是还可以毫不费力地正确处理单个数组/对象字段的特定错误消息,并且应该与通常的错误消息翻译完全兼容。

    创建自定义验证器类

    您应该首先在您的应用文件夹中创建一个 Validator 类(我将它放在app/Validation/Validator.php 下)并像这样实现 passes 方法:

    <?php
    
    namespace App\Validation;
    
    use Illuminate\Support\Arr;
    use Illuminate\Validation\Validator as BaseValidator;
    
    class Validator extends BaseValidator
    {
        /**
         * Determine if the data passes the validation rules.
         *
         * @return bool
         */
        public function passes()
        {
            // Perform the usual rules validation, but at this step ignore the
            // return value as we still have to validate the allowance of the fields
            // The error messages count will be recalculated later and returned.
            parent::passes();
    
            // Compute the difference between the request data as a dot notation
            // array and the attributes which have a rule in the current validator instance
            $extraAttributes = array_diff_key(
                Arr::dot($this->data),
                $this->rules
            );
    
            // We'll spin through each key that hasn't been stripped in the
            // previous filtering. Most likely the fields will be top level
            // forbidden values or array/object values, as they get mapped with
            // indexes other than asterisks (the key will differ from the rule
            // and won't match at earlier stage).
            // We have to do a deeper check if a rule with that array/object
            // structure has been specified.
            foreach ($extraAttributes as $attribute => $value) {
                if (empty($this->getExplicitKeys($attribute))) {
                    $this->addFailure($attribute, 'forbidden_attribute', ['value' => $value]);
                }
            }
    
            return $this->messages->isEmpty();
        }
    }
    

    这实际上将扩展默认的 Validator 类以在 pass 方法上添加 附加检查。检查通过转换为点表示法(以支持数组/对象验证)和分配了至少一个规则的属性之间的键计算数组差异。

    替换容器中默认的Validator

    那么您错过的最后一步是在 服务提供者boot 方法中绑定新的 Validator 类。为此,您只需覆盖Illuminate\Validation\Factory 类的解析器,该类绑定到 IoC 容器中,为 'validator'

    // Do not forget the class import at the top of the file!
    use App\Validation\Validator;
    
    // ...
    
        /**
         * Bootstrap any application services.
         *
         * @return void
         */
        public function boot()
        {
            $this->app->make('validator')
                ->resolver(function ($translator, $data, $rules, $messages, $attributes) {
                    return new Validator($translator, $data, $rules, $messages, $attributes);
                });
        }
    
    // ...
    

    在控制器中的实际使用

    您无需执行任何特定操作即可使用此功能。照常调用validate 方法即可:

    $this->validate(request(), [
        'first_name' => 'required|string|max:40',
        'last_name' => 'required|string|max:40'
    ]);
    

    自定义错误消息

    要自定义错误消息,您只需在 lang 文件中添加一个转换键,其键等于 forbidden_attribute(您可以在 addFailure 方法调用的自定义 Validator 类中自定义错误键名称)。

    示例: resources/lang/en/validation.php

    <?php
    
    return [
        // ...
    
        'forbidden_attribute' => 'The :attribute key is not allowed in the request body.',
    
        // ...
    ];
    

    注意:此实现仅在 Laravel 5.3 中进行了测试。

    【讨论】:

      猜你喜欢
      • 2018-09-15
      • 2012-05-31
      • 1970-01-01
      • 2020-06-03
      • 2018-06-04
      • 2021-07-04
      • 2012-12-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多