Валидация и формы

Правила валидации, 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 — и повышает свои права.

Защита — два слоя:

  1. $fillable / $guarded в модели:
protected $fillable = ['title', 'body'];   // только эти поля можно массово
// Model::unguard() / $guarded = [] — отключить (опасно в публичных формах)

MassAssignmentException — если поле не в $fillable и $guarded = [] не стоит... точнее: поле молча игнорируется (исключение — только при Model::preventSilentlyDiscardingAttributes()).

  1. Брать данные только из валидации:
Post::create($request->validated());   // только провалидированные ключи
// или
$post->fill($request->safe()->only(['title', 'body']));

Правило: никогда не передавать $request->all() в create/update модели напрямую. В dev включить:

Model::preventSilentlyDiscardingAttributes(! app()->isProduction());

— чтобы замечать случайно «съеденные» fillable-поля.