Как стать автором
Обновить

Декомпозиция Form Request в Laravel

Время на прочтение 4 мин
Количество просмотров 10K

Всем привет, сегодня я расскажу как и зачем я структурировал валидацию в 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.

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

Теги:
Хабы:
+9
Комментарии 15
Комментарии Комментарии 15

Публикации

Истории

Работа

PHP программист
175 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн