Декомпозиция Form Request в Laravel
Всем привет, сегодня я расскажу как и зачем я структурировал валидацию в 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.
Специально для ленивых и любознательных я создал репозиторий. Код там довольно простой, поэтому можно просто адаптировать его под себя. Если вы сталкивались в своей практике с похожей проблемой, напишите в комментариях как ее решали.