Проверка Laravel: разрешать только известные свойства / атрибуты, в противном случае проверка не выполняется

#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 для обработки всех случаев.