Form Requests — больше, чем валидация

Запросы форм в Laravel — одна из наиболее недооцененных функций. Возможно, даже «спрятанная» во фреймворке. Можете мне не верить, но даже Taylor Otwell согласен с этим:

Taylor Otwell  Form Request

Правда, — Taylor Otwell

Давайте я покажу вам, что можно сделать с запросами форм и как их можно использовать их для написания красивых и выразительных API.

Что такое запросы форм (Form Requests)?

Классы Form Requests появились в Laravel 5.0 — то есть они существуют с февраля 2015 года! Если мы посмотрим в документацию этой версии, то увидим следующее определение:

Запросы форм — это кастомные классы запросов, содержащие логику валидации.

Итак, официальная документация говорит о том, что запросы форм нужны для валидации. Давайте сгенерируем запрос формы при помощи команды php artisan make:request и посмотрим, что у него внутри.

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ExampleRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            //
        ];
    }
}

Валидация

Мы получаем два метода, которые можно заполнить собственной логикой. Метод authorize содержат логику, может ли быть выполнен запрос. Здесь можно проверить права доступа/гварды текущего пользователя и вернуть true или false. Возврат false приведет к ответу 403 Forbidden. И метод rules, где можно задать свои правила валидации, которые должны применяться к параметрам текущего запроса. Это может быть очень удобно, если вы хотите инкапсулировать логику валидации по отдельным классам.

Чтобы начать использовать класс запроса формы, вам нужно использовать тайпхинт в методе контроллера следующим образом:

class StoreBlogPostController {

    public function __invoke(BlogPostRequest $request) {
    	Post::create($request->validated());
    }

}

Выглядит очень просто, но под капотом происходит много крутых вещей. Используя внедрение метода в методе контроллера, Laravel знает, что запрос должен быть авторизован. Поэтому, прежде чем вызывать метод контроллера, Laravel просматривает результат authorize класса запроса формы. Если он возвращает false, пользователь получает ответ 403. если же true — то Laravel проверяет ваши rules и автоматически проверяет данные запроса на соответствие этому набору правил. Если один или несколько из них не пройдут, то Laravel редиректит пользователя на предыдущую страницу с сообщениями $errors, которые можно использовать для отображения ошибок валидации. Если все прошло успешно — метод authorize вернул true и все rules оказались валидны, то, наконец, вызывается метод контроллера.

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

Ну, раз в документации указано, что основным вариантом использования запросов формы является валидация, то мы уже закончили? Нет — совсем нет! Позвольте мне показать, как еще можно использовать классы запроса формы.

За пределами валидации

Поскольку запросы форм — это просто кастомные классы, то они позволяют добавлять дополнительную логику вашим запросам. Это позволит нам создавать очень выразительные и красивые API для приложений. Я покажу вам, как лично я использую это в своих проектах.

Этот запрос взят из нашего видеокурса и используется, когда срабатывает вебхук от Paddle, который мы используем для продажи курсов:

namespace App\Http\Requests;

use App\Packages\CourseRepository;
use App\Packages\Package;
use Illuminate\Foundation\Http\FormRequest;

class PaddlePurchaseRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }


    public function rules()
    {
        return [];
    }

    public function getPackage(): ?Package
    {
        return CourseRepository::findPackageForPaddleIdle($this->product_id);
    }

    public function isBlackFridayBundle(): bool
    {
        return $this->product_id == config('courses.blackfriday.bundle_id');
    }

    public function isVideoCourse(): bool
    {
        return ! is_null(CourseRepository::findPackageForPaddleIdle($this->product_id));
    }

    public function isTinkerwell(): bool
    {
        return is_null(CourseRepository::findPackageForPaddleIdle($this->product_id));
    }
}

Ого — вообще нет валидации? Как видите, я не использую запрос формы для «предназначенной» цели — или, по крайней мере, не для цели, упомянутой в документации Laravel. Вместо этого я использую их для добавления дополнительных методов, которые я могу использовать в своем контроллере.

Чтобы понять, что делает этот запрос, позвольте мне быстро объяснить, как работает вебхук Paddle. На моей платформе продажи видеокурсов у меня есть несколько «пакетов», например, пакеты «Basic» и «Pro». С этими пакетами связан идентификатор Paddle-ID, чтобы я могу определить, какой продукт был куплен и отправить приветственное сообщение пользователю, добавить отношение к базе данных и т.д.

Поэтому в класс PaddlePurchaseRequest я добавил несколько методов, чтобы я мог получить класс купленного пакета. Это позволит мне вызывать $request->getPackage() в контроллере. Или, в случае приобретенной лицензии Tinkerwell, я могу проверить это с помощью метода $request->isTinkerwell().

Мне нравится добавлять методы в классы запросов, выполняющие логику с данными в текущем запросе. Это могут быть данные, которые активно отправлялись, например, в рамках запроса POST, но это также может быть просто доступ к домену, реферер запроса или текущий пользователь, вошедший в систему.

Это имеет смысл, ведь вы абстрагируете логику, относящуюся к «разрешению чего-то из текущего запроса» в эти классы запросов. Вы даже можете пойти еще дальше и поместить общие методы запроса в абстрактные классы запросов, которые вы будете расширять.

Посмотрите, что Laravel Nova делает в своих классах запросов форм. Это из запроса панели инструментов:

namespace Laravel\Nova\Http\Requests;
use Laravel\Nova\Nova;

class DashboardCardRequest extends NovaRequest
{
    /**
     * Get all of the possible cards for the request.
     *
     * @param  string  $dashboard
     *
     * @return \Illuminate\Support\Collection
     */
    public function availableCards($dashboard)
    {
        if ($dashboard === 'main') {
            return collect(Nova::$defaultDashboardCards)
                ->unique()
                ->filter
                ->authorize($this)
                ->values();
        }

        return Nova::availableDashboardCardsForDashboard($dashboard, $this);
    }
}

Nova не только использует запросы форм, как я рассказывал, — в классе DashboardCardRequest есть метод availableCards, который возвращает все карточки панели инструментов, которые должны быть видны пользователю в текущем запросе. Но Nova также расширяет все свои классы запросов формы из абстрактного класса NovaRequest, который содержит дополнительную логику, такую как возврат модели и ресурса Nova, к которому обращались в этом запросе.

Куда дальше?

Запросы форм могут быть отличным способом инкапсулировать специфичную логику запросов в свои собственные классы. Не ограничивайте их использование только валидацией или авторизации запросов. Рассматривайте их как классы, которые позволяют вам расширить методы работы с определенным типом запросов.

Я уверен, когда вы в следующий раз будете работать с классами запросов, вы найдете множество способов перемещения логики в запросы форм. Таким образом вы сможете создавать более простые и красивые API в своих приложениях.

Автор: Marcel Pociot
Перевод: Demiurge Ash