#php #laravel #validation
#php #laravel #проверка
Вопрос:
Мы создаем конечную точку api, где требуется точность. Мы хотим обеспечить строгую проверку параметров, которые отправляются на сервер.
Если пользователь api отправляет key=value
пару, которая не поддерживается (например. мы разрешаем параметры [first_name, last_name], и пользователь включает неподдерживаемый параметр [country]), мы хотим, чтобы проверка завершилась неудачей.
Пытались создать пользовательский валидатор с именем allowed_attributes
(используется как allowed_attributes:attr1,attr2,...
), но для того, чтобы его можно было использовать в $validationRules
массиве, он должен быть применен к родительскому элементу списка вложенных / дочерних атрибутов (… потому что в противном случае наш пользовательский валидатор не имел доступа к проверяемым атрибутам).
Validator::extend('allowed_attributes', 'AppValidatorsAllowedAttributesValidator@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 (в пользовательском валидаторе)?
Ответ №1:
Я предпочел опубликовать новый ответ, поскольку этот подход отличается от предыдущего и немного более чистый. Поэтому я бы предпочел, чтобы эти два подхода были разделены и не смешивались вместе в одном ответе.
Лучшее решение проблем
После более глубокого изучения исходного кода пространства имен проверки с момента моего последнего ответа я понял, что самым простым способом было бы расширить класс Validator, чтобы повторно реализовать passes()
функцию, чтобы также проверить, что вам нужно.
Преимущество этой реализации также в том, что она корректно обрабатывает конкретные сообщения об ошибках для отдельных полей массива / объекта без каких-либо усилий и должна быть полностью совместима с обычными переводами сообщений об ошибках.
Создайте пользовательский класс валидатора
Сначала вы должны создать класс Validator в папке вашего приложения (я поместил его в app/Validation/Validator.php
) и реализовать метод passes следующим образом:
<?php
namespace AppValidation;
use IlluminateSupportArr;
use IlluminateValidationValidator 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 по умолчанию, добавив дополнительные проверки для метода passes. Проверка вычисляет разницу массива по ключам между входными атрибутами, преобразованными в точечную нотацию (для поддержки проверки массива / объекта), и атрибутами, которым назначено хотя бы одно правило.
Замените средство проверки по умолчанию в контейнере
Тогда последний шаг, который вы пропускаете, — это привязать новый класс Validator в методе boot поставщика услуг. Для этого вы можете просто переопределить распознаватель IlluminateValidationFactory
класса, привязанного к контейнеру IoC, как 'validator'
:
// Do not forget the class import at the top of the file!
use AppValidationValidator;
// ...
/**
* 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
(вы можете настроить имя ключа ошибки в пользовательском классе Validator при addFailure
вызове метода).
Пример: resources/lang/en/validation.php
<?php
return [
// ...
'forbidden_attribute' => 'The :attribute key is not allowed in the request body.',
// ...
];
Примечание: эта реализация была протестирована только в Laravel 5.3.
Комментарии:
1. Спасибо .. все вышесказанное работает на Laravel lumen-framework 5.8
Ответ №2:
Это должно работать для простых пар ключ / значение с этим пользовательским валидатором:
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, если бы вы проверяли массив объектов, этот пользовательский валидатор работал бы, но сообщение об ошибке, которое вы получите, было бы общим для элемента массива, а не для ключа этого элемента массива, который не был разрешен.
Например, вы разрешаете поле products в своем запросе, каждое с идентификатором:
$validationRules = [
'products.*' => 'allowed_attributes:id',
];
Если вы подтвердите запрос, подобный этому:
{
"products": [{
"id": 3
}, {
"id": 17,
"price": 3.49
}]
}
Вы получите сообщение об ошибке в продукте 2, но вы не сможете определить, какое поле вызывает проблему!
Комментарии:
1. Спасибо, я попробую и обновлю вас. Похоже, что основное отличие от нашей реализации заключается в использовании
Validator::extendImplicit
. Кроме того, исправьте, сообщения об ошибках из array не очень хорошо отформатированы; мы внесли изменения в результирующее сообщение об ошибке, чтобы улучшить смысл, но все еще не идеально. Не удалось найти способ отобразить отклоненное введенное пользователем значение в нашем пользовательском сообщении об ошибке, поэтому пришлось жестко запрограммировать валидатор для отправки дополнительного сообщения с помощью$validator->errors()->add(...)
, означает, что при неудачной проверке пользовательским валидатором запускаются два сообщения об ошибках.2. Возможно, в качестве последнего ресурса вы могли бы расширить класс проверки по умолчанию и переопределить логику проверки в соответствии с вашими потребностями. Затем привяжите его в контейнере приложения, чтобы переопределить значение по умолчанию.
3. У Hmm возникла проблема с тем, что средство проверки AllowedAttributes не смогло обнаружить, что атрибут пустой строки был включен во входные данные. например.
["first_name" => "Name", "" => "Value"]
не провалил бы проверку проверки. Мы на laravel 5.3, так что, возможно, это было исправлено в более новых версиях …?4. Я посмотрю на это и дам вам знать. Мы также могли бы запустить чат в stackoverflow для обработки всех случаев.