Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: Валідація

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

9 питань

Валідація перевіряє вхідні дані за набором правил перш ніж їх використати. Laravel має багату систему правил і кілька способів валідації.

1. Метод validate() прямо в контролері (найпростіше):

$validated = $request->validate([
    'title' => ['required', 'string', 'max:255'],
    'email' => ['required', 'email', 'unique:users,email'],
    'age'   => ['nullable', 'integer', 'min:18'],
]);

Якщо перевірка не пройдена, Laravel автоматично:

  • для веб-запитів - робить редирект назад зі старими даними та помилками в сесії (доступні через $errors у Blade);
  • для API (запит очікує JSON) - повертає відповідь 422 зі структурою { "message": ..., "errors": {...} }.

2. FormRequest - для складнішої логіки правила й авторизацію виносять в окремий клас:

php artisan make:request StorePostRequest
public function rules(): array
{
    return ['title' => ['required', 'max:255']];
}

Це розвантажує контролер. Є десятки вбудованих правил (required, email, unique, exists, confirmed, date), власні правила та умовна валідація.

Докладніше в документації: Валідація

Коли валідація не проходить, Laravel перенаправляє користувача назад, кладе помилки в сесію й «спалахом» зберігає введені дані. У шаблоні їх лишається вивести.

<input name="email" value="{{ old('email') }}" @class(['is-invalid' => $errors->has('email')])>

@error('email')
    <p class="error">{{ $message }}</p>
@enderror

Що тут працює:

  • $errors доступна в кожному шаблоні групи web - її додає middleware ShareErrorsFromSession;
  • @error виводить перше повідомлення для поля в змінній $message;
  • old('email') повертає значення, яке користувач надіслав минулого разу. Другим аргументом передають значення за замовчуванням - наприклад, old('email', $user->email) у формі редагування.

Чого не повертають через old(): паролі. Laravel не зберігає в сесії поля password і password_confirmation.

Для JSON-запитів перенаправлення немає - повертається відповідь 422 з об'єктом errors, і показ помилок бере на себе фронтенд.

Докладніше в документації: Відображення помилок валідації

Три правила відповідають на різні питання: чи має поле бути, чи може воно бути порожнім, і чи перевіряти його взагалі.

$request->validate([
    'title'      => ['required', 'string'],      // має бути й не порожнє
    'publish_at' => ['nullable', 'date'],        // може бути порожнім, інакше - дата
    'nickname'   => ['sometimes', 'string'],     // перевіряється, лише якщо поле прийшло
]);
  • required - поле має бути присутнім і непорожнім (не null, не порожній рядок, не порожній масив).
  • nullable - null дозволений, і решта правил для нього не виконуються.
  • sometimes - якщо ключа в запиті немає, правила поля не запускаються взагалі.

Чому nullable потрібен так часто: глобальні middleware TrimStrings і ConvertEmptyStringsToNull перетворюють порожнє поле форми на null. Без nullable правило date вважатиме null недійсною датою, хоча поле необов'язкове.

Де sometimes доречний: часткові оновлення (PATCH), коли клієнт надсилає лише змінені поля. sometimes|required означає «якщо поле прийшло, воно не може бути порожнім».

Докладніше в документації: Зауваження про необов'язкові поля

FormRequest - окремий клас, що містить правила валідації та авторизацію, виносячи їх із контролера.

class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', Post::class);
    }

    public function rules(): array
    {
        return ['title' => ['required', 'max:255']];
    }
}

// $request - вже провалідовано
public function store(StorePostRequest $request)
{
    Post::create($request->validated());
}

Переваги: тонкі контролери, перевикористання правил, метод prepareForValidation() для нормалізації вводу, власні повідомлення в messages().

Докладніше в документації: Form Request валідація

Згенерувати клас, що реалізує ValidationRule:

php artisan make:rule Uppercase
class Uppercase implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (strtoupper($value) !== $value) {
            $fail('Поле :attribute має бути у верхньому регістрі.');
        }
    }
}

$request->validate(['code' => [new Uppercase]]);

Для разових перевірок можна передати замикання прямо в правило: 'field' => [fn ($attr, $value, $fail) => ...].

Докладніше в документації: Власні правила валідації

Через «крапкову» нотацію й зірочку, яка означає кожен елемент масиву.

$request->validate([
    'items'              => ['required', 'array', 'min:1', 'max:50'],
    'items.*.product_id' => ['required', 'integer', 'distinct', 'exists:products,id'],
    'items.*.quantity'   => ['required', 'integer', 'min:1'],
    'tags'               => ['array'],
    'tags.*'             => ['string', 'max:30'],
]);

На що звернути увагу:

  • Перевіряйте сам масив (array, max), а не лише елементи. Без обмеження клієнт надішле 10 000 елементів, і для кожного виконаються запити exists.
  • distinct забороняє повтори - той самий товар двічі в одному замовленні.
  • exists на кожен елемент - окремий запит до бази. Для великих масивів краще перевірити всі ID одним запитом у after().
  • Повідомлення можуть містити позицію елемента: :index (з нуля) чи :position (з одиниці) - «Товар #3: кількість має бути не менше 1».

validated() поверне лише ті ключі елементів, для яких є правила, тож зайві поля всередині елементів масиву теж відкинуться.

Докладніше в документації: Валідація вкладених масивів

Є кілька рівнів - від рядкових правил до логіки в коді.

1. Готові умовні правила:

'company_name' => ['required_if:type,business'],
'vat_number'   => ['required_with:company_name'],
'doctor_name'  => ['exclude_if:has_appointment,false', 'required'],
'discount'     => ['prohibited_unless:role,manager'],

exclude_if прибирає поле і з перевірки, і з validated() - зручно, коли воно не має сенсу за іншої відповіді.

2. Умова кодом:

'role_id' => Rule::requiredIf(fn () => $this->user()->isAdmin()),
'coupon'  => Rule::when($this->boolean('has_coupon'), ['required', 'string']),

3. Перевірки на кілька полів одразу - метод after() запиту форми:

public function after(): array
{
    return [function (Validator $validator) {
        if ($this->date('ends_at') <= $this->date('starts_at')) {
            $validator->errors()->add('ends_at', 'Кінець має бути пізніше за початок.');
        }
    }];
}

Для такого простого випадку вистачить і правила after:starts_at; after() потрібен, коли логіка складніша.

Докладніше в документації: Умовне додавання правил

Правило unique - це SELECT перед вставкою. Між перевіркою й записом інший запит встигає вставити той самий email.

Запит A: SELECT ... email = 'a@x.com'   -> немає
Запит B: SELECT ... email = 'a@x.com'   -> немає
Запит A: INSERT a@x.com                 -> ок
Запит B: INSERT a@x.com                 -> дубль

Що робити:

  1. Унікальний індекс у базі - єдина справжня гарантія. Валідація лишається для зрозумілого повідомлення в нормальному випадку.
  2. Обробити порушення індексу - Laravel кидає UniqueConstraintViolationException, його перетворюють на ту саму помилку валідації. Або createOrFirst(), який робить це сам.
try {
    $user = User::create($data);
} catch (UniqueConstraintViolationException) {
    throw ValidationException::withMessages(['email' => __('validation.unique', ['attribute' => 'email'])]);
}

Деталі правила, про які питають:

  • при оновленні виключають поточний запис: Rule::unique('users')->ignore($user->id); ignore() ніколи не беруть з даних запиту - лише з моделі, інакше клієнт підставить чужий ID;
  • withoutTrashed() не враховує м'яко видалені записи;
  • регістр: Ada@x.com і ada@x.com різні для unique, тож email нормалізують до нижнього регістру ще до валідації.

Докладніше в документації: Правило unique

Для API валідація - це контракт: клієнти парсять помилки програмно, тож формат і поведінка мають бути передбачуваними.

Що Laravel дає з коробки: якщо запит очікує JSON (Accept: application/json), замість перенаправлення повертається 422:

{
    "message": "The email field is required. (and 1 more error)",
    "errors": {
        "email": ["The email field is required."],
        "items.0.quantity": ["The items.0.quantity field must be at least 1."]
    }
}

Вкладені ключі сплющуються в «крапкову» нотацію.

Що варто налаштувати:

  • Невідомі поля. #[FailOnUnknownFields] на запиті форми (або глобально FormRequest::failOnUnknownFields()) відхиляє поля, яких немає в правилах. Помилки клієнта на кшталт emial стають помітними одразу, а не тихо ігноруються.
  • Стабільні повідомлення. Клієнти не мають залежати від тексту - лише від ключів полів і HTTP-коду. Якщо потрібні машинні коди помилок, їх додають власним форматом відповіді.
  • Межі вхідних даних: max для рядків і масивів, ліміт розміру тіла запиту на вебсервері.
  • Мова повідомлень за заголовком Accept-Language, якщо API багатомовний.

Тести: assertUnprocessable() і assertInvalid([...])/assertJsonValidationErrors() фіксують контракт, щоб зміна правил не зламала клієнтів непомітно.

Докладніше в документації: Формат відповіді з помилками валідації