Laravel: Глубокое погружение в FormRequest

Источник: «Laravel Form Requests»
В Laravel валидация форм является ключевым моментом для обеспечения безопасности и точности пользовательских данных. Компонент FormRequest упрощает эту задачу, поскольку правила валидации хранятся в специальном классе, а не в контроллере. Это также делает код более аккуратным и позволяет использовать специальные функции, связанные с запросом, такие как получение модели или изменение типа данных в поле.

FormRequest

Чтобы создать FormRequest для страницы входа в систему, выполним:

php artisan make:request LoginRequest

Далее обновляем сгенерированный класс следующим образом:

class LoginRequest extends FormRequest
{
public function rules()
{
return [
'email' => 'required|string',
'password' => 'required|string',
];
}
}

Для использования достаточно добавить его к соответствующему методу в контроллере:

class LoginController extends Controller
{
public function store(LoginRequest $request)
{
// Ваш код
}
}

Все просто и понятно!

Далее мы рассмотрим некоторые его дополнительные возможности.

Метод: validated

Если вы хотите получить доступ ко всем ключам и значениям, заданным в методе rules, есть простой способ сделать это.

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

Post::create([
'title' => $request->title,
'body' => $request->body,
'published_at' => $request->published_at,
]);

Можно просто использовать метод validated():

Post::create($request->validated());

Метод: afterValidation

Класс FormRequest позволяет вносить изменения на лету, что помогает поддерживать чистоту других классов. Рассмотрим работу с массивом тегов:

В правилах:

public function rules()
{
return [
'title' => 'required|string',
'body' => 'required|string',
'published_at' => 'datetime',
];
}

Синхронизация тегов с нашим постом традиционно выглядит следующим образом:

public function store(StorePostRequest $request) {
$post = Post::create($request->only('title', 'body','published_at'));

if($request->has('tags')) {
$post->tags()->attach(explode(',', $request->tags));
}
}

Обратите внимание, как мы разделили теги, чтобы прикрепить каждый из них к сообщению? Мы можем сделать это непосредственно в FormRequest с помощью метода afterValidation:

class StorePostRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => 'required|string',
'body' => 'required|string',
'published_at' => 'datetime',
];
}

protected function passedValidation() {
$this->merge([
'tags' => $this->tags ? explode(',', $this->tags) : []
]);
}
}

При такой настройке после валидации теги уже будут иметь формат массива. Это упрощает код контроллера:

public function store(StorePostRequest $request) {
$post = Post::create($request->only('title', 'body','published_at'));

$post->tags()->attach($request->tags);
}

Метод: prepareForValidation

Используя метод prepareForValidation, мы можем корректировать вводимые данные непосредственно перед тем, как FormRequest начнёт процесс проверки. Для наших тегов мы хотим, чтобы они были целыми числами.

Сначала настроим ввод тегов:

protected function prepareForValidation() {
$this->merge([
'tags' => $this->tags ? explode(',', $this->tags) : []
]);
}

Теперь обновлённый класс FormRequest должен иметь вид:

class StorePostRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => 'required|string',
'body' => 'required|string',
'published_at' => 'datetime',
'tags' => 'array',
'tags.*' => 'integer',
];
}

protected function prepareForValidation() {
$this->merge([
'tags' => $this->tags ? explode(',', $this->tags) : []
]);
}
}

Контроллер, несмотря на эти изменения, по-прежнему будет рассматривать теги как массив:

public function store(StorePostRequest $request) {
$post = Post::create($request->only('title', 'body','published_at'));

$post->tags()->attach($request->tags);
}

Метод: user

Из класса запроса можно напрямую обратиться к аутентифицированному пользователю:

public function store(StorePostRequest $request) {
$post = $request->user()->posts()->create(
$request->only('title', 'body','published_at')
);
}

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

Понимание валидации

Когда вы используете FormRequest в сигнатуре метода, Laravel автоматически обрабатывает валидацию за вас. Это означает, что вам не нужно вручную вызывать $request->validate() в контроллере. Давайте разберёмся, как происходит эта магия.

Для анализа компонента валидации хорошей отправной точкой является поиск случаев использования метода rules из нашего класса StorePostRequest.

Вы можете быть удивлены, обнаружив, что функция PHPStorm "find usages" не обнаруживает использования этого метода непосредственно в нашем пользовательском классе FormRequest. Но если поискать его в родительском классе FormRequest, то можно обнаружить, что он используется в методе createDefaultValidator.

protected function createDefaultValidator(ValidationFactory $factory)
{
$rules = method_exists($this, 'rules') ? $this->container->call([$this, 'rules']) : [];

$validator = $factory->make(
$this->validationData(), $rules,
$this->messages(), $this->attributes()
)->stopOnFirstFailure($this->stopOnFirstFailure);

if ($this->isPrecognitive()) {
$validator->setRules(
$this->filterPrecognitiveRules($validator->getRulesWithoutPlaceholders())
);
}

return $validator;
}

Класс FormRequest использует сервис-контейнер Laravel при вызове метода rules. Такой подход выгоден тем, что позволяет выполнять внедрение зависимостей непосредственно в метод rules, то есть любой необходимый сервис или класс может быть автоматически разрешён и внедрён.

Теперь посмотрим, как используется createDefaultValidator

protected function getValidatorInstance()
{
if ($this->validator) {
return $this->validator;
}

$factory = $this->container->make(ValidationFactory::class);

if (method_exists($this, 'validator')) {
$validator = $this->container->call([$this, 'validator'], compact('factory'));
} else {
$validator = $this->createDefaultValidator($factory);
}

if (method_exists($this, 'withValidator')) {
$this->withValidator($validator);
}

if (method_exists($this, 'after')) {
$validator->after($this->container->call(
$this->after(...),
['validator' => $validator]
));
}

$this->setValidator($validator);

return $this->validator;
}

При валидации с помощью FormRequest Laravel выполняет ряд шагов. Сначала Laravel проверяет, есть ли в FormRequest собственный класс Validator; если нет, то используется createDefaultValidator. Затем Laravel проверяет, есть ли какие-либо последующие хуки, которые необходимо запустить.

Продолжая исследование, мы видим, что вызывается getValidatorInstance.

Далее расположен трейт ValidatesWhenResolvedTrait. Этот трейт применяется ко всем FormRequest и содержит методы для проверки входных данных при определении класса FormRequest.

public function validateResolved()
{
$this->prepareForValidation();

if (! $this->passesAuthorization()) {
$this->failedAuthorization();
}

$instance = $this->getValidatorInstance();

if ($this->isPrecognitive()) {
$instance->after(Precognition::afterValidationHook($this));
}

if ($instance->fails()) {
$this->failedValidation($instance);
}

$this->passedValidation();
}

Здесь мы сталкиваемся с методами prepareForValidation и passedValidation, о которых мы говорили ранее.

Перейдём к рассмотрению того, как используется метод validateResolved().

namespace Illuminate\Foundation\Providers;

class FormRequestServiceProvider extends ServiceProvider
{
public function boot()
{
$this->app->afterResolving(ValidatesWhenResolved::class, function ($resolved) {
$resolved->validateResolved();
});

$this->app->resolving(FormRequest::class, function ($request, $app) {
$request = FormRequest::createFrom($app['request'], $request);

$request->setContainer($app)->setRedirector($app->make(Redirector::class));
});
}
}

Действительно, в FormRequestServiceProvider Laravel устанавливает хук afterResolving, запускающий трейт ValidatesWhenResolved для вызова метода validateResolved, который эффективно выполняет валидацию.

Это и есть основной процесс выполнения валидации FormRequest при включении его в метод контроллера.

Дополнительные материалы

Предыдущая Статья

Менее известные трюки и советы по Composer, которые нужно знать

Следующая Статья

Более современный сброс CSS