Дублирование правил валидации между бэкендом и фронтендом — одна из самых раздражающих рутин при разработке форм. Вы описываете правила в FormRequest на Laravel, а потом переписываете ту же логику на JavaScript, чтобы пользователь видел ошибки до отправки формы. Любое изменение приходится синхронизировать в двух местах, и рано или поздно они расходятся.
Laravel Precognition решает эту проблему элегантно: «живая» валидация на фронтенде работает на тех же правилах, что и на сервере, без единой строки дублирования. В этой статье разберём, как это устроено, и соберём рабочую форму на Alpine.js и Blade с пошаговыми примерами.
Как работает Precognition
Идея в самом названии — «предвидение». Фронтенд отправляет на сервер так называемый «прекогнитивный» запрос с заголовком Precognition. Получив его, Laravel выполняет все middleware маршрута и резолвит зависимости контроллера, включая валидацию FormRequest, но не выполняет тело метода контроллера. То есть пользователь не создаётся, письмо не отправляется, заказ не оформляется — выполняется только проверка данных, а результат валидации возвращается на фронт.
Благодаря этому одни и те же правила работают и при «живой» проверке полей по мере ввода, и при финальной отправке формы. Источник истины — один: ваш FormRequest.
Шаг 1. Подключаем middleware к маршруту
Чтобы маршрут начал понимать прекогнитивные запросы, добавьте к нему middleware HandlePrecognitiveRequests. Правила валидации выносим в отдельный FormRequest:
use App\Http\Requests\StoreUserRequest;
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
Route::post('/users', function (StoreUserRequest $request) {
// Этот код выполнится только при реальной отправке,
// но не при прекогнитивном запросе.
User::create($request->validated());
return back();
})->middleware([HandlePrecognitiveRequests::class]);
Сам FormRequest создаётся обычной командой Artisan и выглядит максимально привычно:
php artisan make:request StoreUserRequest
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreUserRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users,email'],
];
}
}
Шаг 2. Устанавливаем фронтенд-хелперы
Precognition поставляет небольшие клиентские библиотеки под Vue, React и Alpine. Поскольку Alpine.js отлично вписывается в классический стек Blade и не требует сборки SPA, возьмём именно его. Устанавливаем пакет через npm:
npm install laravel-precognition-alpine
Затем регистрируем плагин Precognition в resources/js/app.js:
import Alpine from 'alpinejs';
import Precognition from 'laravel-precognition-alpine';
window.Alpine = Alpine;
Alpine.plugin(Precognition);
Alpine.start();
Шаг 3. Собираем форму с живой валидацией
После регистрации плагина в Alpine появляется «магия» $form. Она принимает HTTP-метод, целевой URL и объект с начальными данными. Чтобы запустить живую валидацию, на событие change каждого поля вызываем form.validate() с именем поля:
<form x-data="{
form: $form('post', '/users', {
name: '',
email: '',
}),
}" @submit.prevent="form.submit()">
@csrf
<label for="name">Имя</label>
<input
id="name"
name="name"
x-model="form.name"
@change="form.validate('name')"
/>
<template x-if="form.invalid('name')">
<div class="error" x-text="form.errors.name"></div>
</template>
<label for="email">Email</label>
<input
id="email"
name="email"
x-model="form.email"
@change="form.validate('email')"
/>
<template x-if="form.invalid('email')">
<div class="error" x-text="form.errors.email"></div>
</template>
<button :disabled="form.processing">
Создать пользователя
</button>
</form>
Теперь, когда пользователь меняет значение поля, на сервер уходит «отложенный» (debounced) прекогнитивный запрос, а ошибки автоматически попадают в объект form.errors. Никакого ручного fetch и парсинга ответов — библиотека делает это сама.
Шаг 4. Управляем состоянием формы
Объект формы предоставляет набор удобных свойств и методов для отрисовки состояния интерфейса. Вот самые востребованные на практике.
Интервал debounce по умолчанию можно изменить, если хочется реже дёргать сервер:
form.setValidationTimeout(1000); // мс
Пока валидационный запрос «в полёте», свойство form.validating равно true — удобно показать индикатор:
<template x-if="form.validating">
<div>Проверяем…</div>
</template>
Проверить, прошло ли конкретное поле валидацию, помогают методы valid() и invalid(), а наличие любых ошибок — свойство hasErrors:
<template x-if="form.valid('email')">
<span>✅</span>
</template>
<template x-if="form.hasErrors">
<div>Исправьте ошибки в форме.</div>
</template>
Поле отображается как валидное или невалидное только после того, как оно изменилось и сервер вернул ответ — пустую форму Precognition не «красит» раньше времени.
Шаг 5. Валидация шагов в мастере (wizard)
Частая задача — многошаговая форма, где перед переходом к следующему шагу нужно проверить группу полей, даже если пользователь к части из них ещё не прикасался. Для этого validate() принимает объект конфигурации с ключом only и колбэками onSuccess / onValidationError:
<button
type="button"
@click="form.validate({
only: ['name', 'email', 'phone'],
onSuccess: (response) => nextStep(),
onValidationError: (response) => scrollToError(),
})"
>Следующий шаг</button>
Шаг 6. Разные правила для проверки и отправки
Иногда часть проверок имеет смысл выполнять только при финальной отправке. Классический пример — проверка пароля на «утечку» через сервис Have I Been Pwned: гонять её на каждый change накладно. Метод isPrecognitive() внутри FormRequest позволяет различать прекогнитивный запрос и реальную отправку:
use Illuminate\Validation\Rules\Password;
public function rules(): array
{
return [
'password' => [
'required',
$this->isPrecognitive()
? Password::min(8)
: Password::min(8)->uncompromised(),
],
];
}
То же касается файлов. По умолчанию Precognition не загружает и не валидирует файлы при прекогнитивных запросах, чтобы не гонять тяжёлые данные по сети на каждое изменение. Поэтому правило required для файла стоит включать только для полной отправки:
public function rules(): array
{
return [
'avatar' => [
...$this->isPrecognitive() ? [] : ['required'],
'image',
'mimes:jpg,png',
],
];
}
Шаг 7. Не забываем про побочные эффекты middleware
Раз прекогнитивный запрос проходит через все middleware маршрута, важно убедиться, что они не делают лишнего. Например, middleware, считающий «активность» пользователя, не должен учитывать проверочные запросы. Тот же isPrecognitive() доступен и в объекте Request:
public function handle(Request $request, Closure $next): mixed
{
if (! $request->isPrecognitive()) {
Interaction::incrementFor($request->user());
}
return $next($request);
}
Шаг 8. Тестируем прекогнитивные запросы
В тестах Laravel есть хелпер withPrecognition(), который добавляет нужный заголовок, и ассерт assertSuccessfulPrecognition(). Так можно убедиться, что проверка прошла, а в базу при этом ничего не записалось:
it('validates registration form with precognition', function () {
$response = $this->withPrecognition()
->post('/users', [
'name' => 'Иван Иванов',
]);
$response->assertSuccessfulPrecognition();
expect(User::count())->toBe(0);
});
Итоги
Laravel Precognition убирает целый класс багов, связанных с рассинхронизацией правил валидации между сервером и клиентом. Вы описываете правила один раз в FormRequest, добавляете middleware HandlePrecognitiveRequests к маршруту и подключаете лёгкий фронтенд-хелпер — после чего получаете живую валидацию «из коробки» на Alpine, Vue или React.
На что обратить внимание при внедрении: помните про isPrecognitive() для тяжёлых правил и файлов, настраивайте debounce под нагрузку и проверяйте побочные эффекты в middleware. С этими нюансами Precognition становится одним из самых практичных инструментов в экосистеме Laravel для форм любой сложности — от простой регистрации до многошаговых мастеров.