我们正在构建需要精确度的api端点。我们要对POST / PUT到服务器的参数实施严格的验证。
如果api用户发送了不支持的key=value
对(例如,我们允许参数[first_name,last_name],并且用户包括不受支持的参数[country]),则我们希望验证失败。
曾尝试构建一个名为allowed_attributes
(用作allowed_attributes:attr1,attr2,...
)的自定义验证器,但要使其在$validationRules
数组中可用,必须将其应用于a的父项嵌套/子属性的列表(...否则,我们的自定义验证器将无法访问正在验证的属性)。
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属性的列表(在自定义验证程序中)?
答案 0 :(得分:0)
使用此自定义验证程序,它应可用于简单的键/值对:
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_name
和parent.last_name
上的规则;相反,将其保留为第一条规则不会触发first_name
和last_name
的验证。
这意味着您最终可以从allowed_attributes
规则的参数列表中删除具有进一步验证逻辑的属性。
例如,如果您只需要 first_name 和 last_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的验证逻辑,如果您要验证对象数组,则此自定义验证器将起作用,但是您将获得的错误消息将在数组项上通用,而不是键上不允许的那个数组项。
例如,您在请求中允许一个产品字段,每个字段的ID为:
$validationRules = [
'products.*' => 'allowed_attributes:id',
];
如果您验证这样的请求:
{
"products": [{
"id": 3
}, {
"id": 17,
"price": 3.49
}]
}
您会在产品2上看到一个错误,但是您将无法确定是哪个字段引起了该问题!
答案 1 :(得分:0)
我更喜欢发布一个新的答案,因为该方法与以前的方法有所不同,并且更加简洁。因此,我宁愿将两种方法分开,而不是在同一答案中混在一起。
自从我上次回答以来,对Validation命名空间的源代码进行了更深入的研究后,我发现最简单的方法是扩展Validator类以补充passes()
函数来检查所需的内容。
此实现的好处是还可以正确处理单个数组/对象字段的特定错误消息,而无需费力,并且应与常规错误消息转换完全兼容。
您应该首先在您的应用文件夹中创建一个 Validator 类(我将其放置在app/Validation/Validator.php
下),并实现如下的 pass 方法:
<?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 类,以在passs方法上添加其他检查。该检查通过键计算输入属性(转换为点表示法(以支持数组/对象验证)和至少分配了一个规则的属性之间的键)之间的数组差异。< / p>
那么,您错过的最后一步是在服务提供商的 boot 方法中绑定新的Validator类。为此,您可以覆盖Illuminate\Validation\Factory
类的解析器,该类以'validator'
绑定到IoC容器中:
// 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中进行了测试。