Всем привет, сегодня я расскажу как и зачем я структурировал валидацию в Laravel.
Вспомним как работает Form Request
Form Request — это класс где мы описываем правила валидации для входящих данных. Обычно класс содержит набор правил под запрос из клиента. Мы можем его декларировать в контроллере, и через контейнер в Laravel он автоматически проверит данные на соответствии нашим правилам и через внутренние механизмы фреймворка выдаст ответ клиенту.
Для примера нам надо обновить профиль пользователя. Form Request может выглядеть вот так:
namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class UpdateUserProfile extends FormRequest { public function rules(): array { return [ 'email' => ['required', 'email'], 'name' => ['required', 'alpha'], 'age' => ['integer', 'max:120'], ]; } public function messages():array { return [ 'email.required' => 'Email необходимо заполнить email' ]; } }
Выглядит знакомо и ничего не обычного. Схематично я нарисовал ниже как это выглядит. Все правила в одном Form Request являются неразделимыми и вроде это как не должно быть проблемой…

Но, давай-те представим, что мы разрабатываем телеграм бота и функция обновления пользователя необходима и здесь. Взаимодействия пользователя через бота несколько отличается от привычного взаимодействия пользователя на сайте.
Бот задает последовательные вопросы, в момент получает ответы и сразу должен их провалидировать. Будет странно — если в конце бот сообщит «еmail был не верный». В моем представлении бот ведет диалог, вопрос за вопросом, контролируя верность введенных данных.
Как Form Request решит эту проблему?
Итак у нас уже есть один Form Request который валидирует обычную форму из сайта.
-
Мы можем постараться изменить наш Form Request под нашу задачу. Добавить необязательные правила(но это шанс из формы не отправлять все данные). Еще мне не нравится этот подход, так как он двухсмысленнен и нечитаем. Вне контекста мы(или коллега) позже не вспомним что валидируется и при каком случае.
-
Добавить обычную валидацию. Тут есть нарушение принципа «Don’t repeat yourself». Если у нас добавиться новое правило(а оно обязательно будет), то нам надо не забыть его изменить уже в двух местах.
public function store(Request $request) { $validated = $request->validate([ 'email' => ['required', 'email'] ]); }
Декомпозиция правил валидации
Мне в голову пришла другая идея, рассматривать каждое поле(field) как отдельное ValidatorValue.

Начнем с того как будет выглядеть наш предыдущий Form Request.
class UpdateUserProfile extends FormRequestDecompose { public function rules(): array { return [ new UserEmail(auth()->user()->id), new UserName(), new UserAge(), ]; } }
-
Мы отнаследовались от моего базового класса FormRequestDecompose, который содержит в себе некую логику по обработки объектов ValidatorValue.
-
В список правил, мы добавляем теперь просто объекты.
-
Этот способ не исключает обычное использование ключ и список правил в виде массива(для примера)
Как это работает?
Каждый класс реализует интерфейс ValidatorValue. В конструктор передаются внешние данные на которые мы можем опираться во время валидации. Еще в конструктор я передаю атрибут, если он может изменяться, но это зависит уже кода. В методе getRules описывается набор правил валидации, соответственно в методе getMessages кастомизированные ответы на эти правила(если они имеются).
class UserEmail implements ValidatorValue { private $attribute; private $exceptUserId; public function __construct(int $userId, string $attribute = 'email') { $this->exceptUserId = $userId; $this->attribute = $attribute; } public function getAttribute(): string { return $this->attribute; } public function getRules(): array { return [ 'required', 'email', "unique:users,email,{$this->exceptUserId}", ]; } public function getMessages(): array { return [ "{$this->attribute}.email" => 'Пожалуйста, укажите корректный email', "{$this->attribute}.required" => 'Пожалуйста, укажите email', "{$this->attribute}.unique" => 'Email уже зарегестрирован' ]; } }
interface ValidatorValue { /** * Should return list rules * @example ['required','email','unique:users,email']; * @return array */ public function getRules(): array; /** * @return string */ public function getAttribute(): string; /** * @return array */ public function getMessages(): array; }
Как теперь мы можем решить нашу проблему с валидацией данных от бота?
В Laravel я использую BotMan, это фреймворк для работы с ботами и заточенный под Laravel.
Итак, в моем случае использую Facade для валидации. Все нужные данные и конфигурации мы передаем из нашего объекта.
$validatorUserEmail = UserEmail(auth()->user()->id); $this->validator = Validator::make([ $validatorUserEmail->getAttribute() => $answerFromUser ],[ $validatorUserEmail->getAttribute() => $validatorUserEmail->getRules() ], $validatorUserEmail->getMessages()); if ($this->validator->fails() === false) { // ... }
К сожалению, это не тот клиентский код о котором мы мечтали.
Но мы можем написать небольшую обертку и выглядеть это будет более изящно:
if ($this->validate($answer->getText(), new UserEmail($this->user->id)) { // ... }
Стоит упомянуть, что как и во фреймворке, я предварительно регистрирую в контейнере FormRequestDecompose, для его корректной работы.
В этом подходе мне нравится, что все правила находятся в одном месте. Мы можем его использовать как в Form Request так и при обычной валидации. Во-вторых название класса может быть более выразительным для предметной области, например: ConsumerEmail, SellerPersonalPhone.
Специально для ленивых и любознательных я создал репозиторий. Код там довольно простой, поэтому можно просто адаптировать его под себя. Если вы сталкивались в своей практике с похожей проблемой, напишите в комментариях как ее решали.
ссылка на оригинал статьи https://habr.com/ru/post/557156/
Добавить комментарий