Middle: питання на співбесіді з теми «Запити й відповіді»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
3 питання
Form Request перевіряється ще до виклику методу контролера - коли контейнер створює його як аргумент. Порядок кроків:
authorize()-falseчи заборонена відповідь політики →AuthorizationException→ 403. Якщо методу немає, запит вважається дозволеним;prepareForValidation()- підготувати дані (нормалізувати, об'єднати);rules()+ валідатор, разом зafter()- додаткові перевірки після основних правил;- при помилці -
failedValidation(): редирект назад з помилками чи 422 для JSON; - при успіху -
passedValidation(), і лише потім виконується контролер.
after() - перевірки, яким потрібні вже провалідовані значення чи кілька полів одразу:
public function after(): array
{
return [
function (Validator $validator) {
if ($this->date('ends_at') <= $this->date('starts_at')) {
$validator->errors()->add('ends_at', 'Кінець має бути пізніше за початок.');
}
},
new EnsureSlotIsFree, // клас з __invoke(Validator $validator)
];
}
Налаштування поведінки - властивостями чи, у Laravel 13, атрибутами класу:
#[StopOnFirstFailure] // зупинитися на першому полі з помилкою
#[RedirectToRoute('checkout.show')] // куди повертати замість back()
#[ErrorBag('checkout')] // окремий мішок помилок для кількох форм на сторінці
#[FailOnUnknownFields] // помилка на полях, яких немає в rules()
final class CheckoutRequest extends FormRequest { /* ... */ }
FailOnUnknownFields - новинка Laravel 13: якщо клієнт надіслав поле, якого немає в rules(), кожне таке поле отримує помилку prohibited. Захищає від масового присвоєння й опечаток у назвах полів на фронтенді. Глобально - FormRequest::failOnUnknownFields() у сервіс-провайдері.
Власна реакція на помилку - перевизначити failedValidation():
protected function failedValidation(Validator $validator): void
{
throw new HttpResponseException(response()->json([
'title' => 'Validation failed',
'errors' => $validator->errors(),
], 422));
}
Для API це зазвичай краще зробити один раз у bootstrap/app.php для всіх запитів, а не в кожному класі.
Що пам'ятати:
authorize()не замінює політик: вона виконується до валідації, тож дані запиту ще не перевірені - звертатися до них обережно;validated()іsafe()повертають лише перевірені поля - саме їх передають у модель, а неall();after()виконується навіть після помилок основних правил - перевірки в ньому мають враховувати, що поля можуть бути порожніми чи некоректними.
Докладніше в документації: Валідація: додаткова перевірка у Form Request
Усі значення з HTTP-запиту - рядки (чи масиви рядків). input('active') поверне "0", "false", "on" чи null, і if ($request->input('active')) для "false" дасть true. Laravel має методи, що одразу приводять значення до потрібного типу.
Типізовані методи:
$request->string('name')->trim(); // Stringable - зручні методи рядків
$request->integer('per_page', 15); // int, за замовчуванням 15
$request->float('price');
$request->boolean('subscribe'); // true для "1", "true", "on", "yes"
$request->date('birthday'); // Carbon чи null
$request->date('starts_at', 'Y-m-d', 'Europe/Kyiv');
$request->enum('status', OrderStatus::class); // case enum чи null
$request->enums('statuses', OrderStatus::class); // масив case
$request->array('ids');
$request->collect('items'); // колекція
boolean() особливо корисний для чекбоксів: незазначений чекбокс взагалі не відправляється, і метод поверне false.
enum() повертає null для невідомого значення замість винятку - безпечно для фільтрів у рядку запиту.
Перевірки наявності:
| Метод | true, коли |
|---|---|
has('name') |
ключ є в запиті (навіть порожній) |
filled('name') |
ключ є і значення не порожнє |
missing('name') |
ключа немає |
isNotFilled('name') |
ключа немає чи значення порожнє |
anyFilled(['email', 'phone']) |
хоча б одне заповнене |
Умовна обробка без if:
$query = Order::query();
$request->whenFilled('status', fn (string $status) => $query->where('status', $status));
$request->whenHas('archived', fn () => $query->onlyTrashed());
Де це доречно, а де ні:
- фільтри, сортування, пагінація з рядка запиту - типізовані методи з безпечними значеннями за замовчуванням ідеальні;
- дані для збереження - через Form Request і
validated(): правилаboolean,date,Rule::enum()перевіряють і відхиляють неправильне значення з помилкою для користувача, а не тихо підставляютьnull; - у Form Request ті самі типізовані методи доступні через
$this->safe():$request->safe()->integer('quantity').
Інколи дані треба нормалізувати до валідації: прибрати пробіли з телефону, згенерувати slug з назви, привести email до нижнього регістру.
merge і mergeIfMissing:
$request->merge(['slug' => Str::slug($request->input('title'))]);
$request->mergeIfMissing(['locale' => 'uk']); // лише якщо поля немає
У Form Request - prepareForValidation():
final class StorePostRequest extends FormRequest
{
protected function prepareForValidation(): void
{
$this->merge([
'slug' => Str::slug($this->input('slug') ?: $this->input('title')),
'phone' => preg_replace('/\D+/', '', (string) $this->input('phone')),
]);
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'slug' => ['required', 'alpha_dash', Rule::unique('posts')],
'phone' => ['nullable', 'digits_between:10,12'],
];
}
}
Правила перевіряють уже нормалізоване значення: унікальність slug перевіряється для того slug, який справді буде збережено.
Після валідації - passedValidation() - для перетворень, які не мають впливати на перевірку (наприклад, хешування).
Глобальна нормалізація вже є: middleware TrimStrings обрізає пробіли, а ConvertEmptyStringsToNull перетворює порожні рядки на null. Через це nullable правила й порожні поля працюють очікувано. Виключити поля (наприклад, пароль) можна в bootstrap/app.php.
Чому обережно:
mergeзмінює спільний об'єкт запиту. Після нього змінене значення бачать усі: middleware, що виконуються далі, слухачі, логування. Нормалізація в одному контролері раптово впливає на інший код;- логіка ховається: читач контролера не бачить, що
phoneуже не те, що надіслав клієнт; - не підміняйте значення, які мають бути помилкою. Якщо
statusневідомий, правильна відповідь - помилка валідації, а не тихеmerge(['status' => 'draft']); - не додавайте через
mergeдані, що не повинні приходити від клієнта (user_id, роль), щоб «пройти валідацію». Такі поля встановлюються явно при збереженні, а не потрапляють у вхідні дані.
Альтернатива для обчислюваних полів: не змінювати запит, а додати значення при збереженні: Post::create([...$request->validated(), 'user_id' => $request->user()->id]).