Декомпозиция 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.

Специально для ленивых и любознательных я создал репозиторий. Код там довольно простой, поэтому можно просто адаптировать его под себя. Если вы сталкивались в своей практике с похожей проблемой, напишите в комментариях как ее решали.

ссылка на оригинал статьи https://habr.com/ru/post/557156/