Валидация и формы
Правила валидации, Form Request, кастомные правила, обработка ошибок.
Junior Как валидировать входящие данные в контроллере? Основные правила.
public function store(Request $request)
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'email' => 'required|email|unique:users,email',
'age' => 'nullable|integer|min:18|max:120',
'tags' => 'array',
'tags.*' => 'string|distinct',
'avatar' => 'image|max:2048|mimes:jpg,png',
'password' => 'required|confirmed|min:8', // password_confirmation
'role' => ['required', Rule::in(['admin', 'user'])],
]);
// $validated — только провалидированные поля
}
Частые правила: required, nullable, sometimes, string, integer, numeric, boolean, email, url, date, date_format:Y-m-d, before:today, exists:table,column, unique, in, regex, same:field, different:field, required_if:field,value, bail (остановиться на первой ошибке).
При ошибке: редирект назад с ошибками и old-инпутом (web) или JSON 422 (API).
@error('title') <span>{{ $message }}</span> @enderror
<input value="{{ old('title') }}">
Middle Что такое Form Request и какие преимущества он даёт?
Form Request — отдельный класс запроса с валидацией и авторизацией:
php artisan make:request StorePostRequest
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Post::class);
}
public function rules(): array
{
return ['title' => 'required|max:255', 'body' => 'required'];
}
public function messages(): array // кастомные сообщения
{
return ['title.required' => 'Укажите заголовок'];
}
protected function prepareForValidation(): void // нормализация до валидации
{
$this->merge(['slug' => Str::slug($this->title)]);
}
}
Использование — type-hint в методе; валидация происходит до входа в контроллер:
public function store(StorePostRequest $request)
{
Post::create($request->validated());
}
Преимущества: контроллер чистый, правила переиспользуемы, авторизация рядом с правилами, validated()/safe() отдают только разрешённые поля.
Middle Как создать кастомное правило валидации?
Класс-правило:
php artisan make:rule UppercaseSlug
class UppercaseSlug implements ValidationRule // Laravel 10+
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (strtoupper($value) !== $value) {
$fail('Поле :attribute должно быть в верхнем регистре.');
}
}
}
'title' => ['required', new UppercaseSlug],
Замыкание на месте:
'email' => [function (string $attribute, mixed $value, Closure $fail) {
if (! str_ends_with($value, '@company.com')) {
$fail('Только корпоративная почта.');
}
}],
Rule-объекты фреймворка для сложных случаев:
use Illuminate\Validation\Rule;
'email' => [
Rule::unique('users')->ignore($this->user), // unique с исключением
Rule::exists('domains')->where('active', true),
Rule::when($isAdmin, ['required'], ['nullable']),
Rule::requiredIf(fn () => $this->type === 'business'),
],
Старый путь — Validator::extend() в провайдере; в современных проектах предпочитают классы-правила.
Middle Как валидировать файлы, массивы и вложенные данные?
Файлы:
'avatar' => 'required|image|mimes:jpeg,png|max:2048|dimensions:min_width=200',
// или fluent:
'file' => ['required', File::image()->max(2048)->dimensions(Rule::dimensions()->minWidth(200))],
$request->file('avatar')->store('avatars', 'public'); // storage/app/public/avatars
$request->hasFile('avatar'); $file->isValid();
Массивы и вложенность — «звёздочки»:
'items' => 'required|array|min:1',
'items.*.id' => 'required|integer|exists:products,id',
'items.*.qty' => 'required|integer|min:1',
'user.email' => 'email', // вложенный ключ
Условная валидация:
'card' => 'required_if:payment,card',
'company' => 'exclude_if:type,personal|string', // не включать в validated
Отдельный валидатор вне запроса:
$validator = Validator::make($data, $rules);
if ($validator->fails()) { $validator->errors(); }
$validator->after(fn ($v) => ...) — доп. проверки после основных правил.
Middle Почему mass assignment опасен и как validated()/fillable защищают?
Проблема: Model::create($request->all()) записывает ЛЮБЫЕ поля из запроса. Злоумышленник добавляет в форму is_admin=1 или role=admin — и повышает свои права.
Защита — два слоя:
$fillable/$guardedв модели:
protected $fillable = ['title', 'body']; // только эти поля можно массово
// Model::unguard() / $guarded = [] — отключить (опасно в публичных формах)
MassAssignmentException — если поле не в $fillable и $guarded = [] не стоит... точнее: поле молча игнорируется (исключение — только при Model::preventSilentlyDiscardingAttributes()).
- Брать данные только из валидации:
Post::create($request->validated()); // только провалидированные ключи
// или
$post->fill($request->safe()->only(['title', 'body']));
Правило: никогда не передавать $request->all() в create/update модели напрямую. В dev включить:
Model::preventSilentlyDiscardingAttributes(! app()->isProduction());
— чтобы замечать случайно «съеденные» fillable-поля.