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

Питання на співбесіді з Laravel

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

378 питань

Фіксація сесії (session fixation): зловмисник отримує ідентифікатор сесії на сайті як гість і якимось чином змушує жертву користуватися саме ним (через вразливий піддомен, що встановлює cookie, через XSS чи спільний комп'ютер). Жертва входить - сесія з відомим зловмиснику ідентифікатором тепер автентифікована, і він заходить в акаунт.

Захист - новий ідентифікатор при зміні рівня привілеїв:

if (Auth::attempt($credentials, $request->boolean('remember'))) {
    $request->session()->regenerate();

    return redirect()->intended('/dashboard');
}

regenerate() видає новий ідентифікатор, зберігаючи дані сесії. Старий ідентифікатор більше нічого не дає. Starter kits і Fortify роблять це самі, але у власному контролері входу про це легко забути.

Вихід - три кроки:

Auth::logout();
$request->session()->invalidate();       // видалити дані й видати новий ідентифікатор
$request->session()->regenerateToken();  // новий CSRF-токен

return redirect('/');
  • Auth::logout() лише прибирає користувача з гарду - решта даних сесії (кошик, флеш, дані іншого гарду) лишається;
  • invalidate() очищує сесію і знищує запис у сховищі, тож старий ідентифікатор не можна використати;
  • regenerateToken() - щоб CSRF-токен з попередньої сесії не працював.

Інші моменти, де варто регенерувати:

  • підвищення привілеїв (вхід в адмін-режим, імперсонація й вихід з неї);
  • зміна пароля - разом з Auth::logoutOtherDevices($password), щоб завершити сесії на інших пристроях.

Що ще захищає сесію:

  • cookie з HttpOnly, Secure і SameSite=Lax - налаштування в config/session.php;
  • SESSION_DOMAIN без зайвої крапки: cookie для .example.com бачать усі піддомени, і вразливий піддомен стає точкою атаки;
  • обмежений lifetime і expire_on_close для чутливих застосунків;
  • для API з токенами (Sanctum) аналог - відкликання токенів при виході й зміні пароля.

Перевірка в тесті: ідентифікатор сесії після входу має відрізнятися від початкового - expect(session()->getId())->not->toBe($before).

Докладніше в документації: Регенерація ідентифікатора сесії

Драйвер database - за замовчуванням у нових застосунках:

  • таблиця sessions з user_id, IP і user agent - можна показати користувачу активні сесії й завершити їх;
  • не потрібна додаткова інфраструктура;
  • кожен запит пише в базу (оновлення last_activity), на навантаженому сайті це помітне навантаження;
  • очищення старих записів - через «лотерею»: 'lottery' => [2, 100] означає, що приблизно у 2 зі 100 запитів Laravel видаляє протерміновані сесії. На великій таблиці це раптово повільний запит у випадкового користувача.

Драйвер redis:

  • швидкий, не навантажує основну базу;
  • записи протерміновуються самим Redis через TTL - лотерея не потрібна (для драйверів на основі кешу очищення нічого не робить);
  • але немає зручного запиту «всі сесії користувача» - для такої функції доведеться зберігати зв'язок окремо;
  • налаштування Redis важливе: якщо в тому самому екземплярі кеш з політикою витіснення allkeys-lru, при нестачі пам'яті Redis видалятиме й сесії - користувачів почне розлогінювати. Сесії краще в окремій базі Redis чи екземплярі з noeviction.

Чого уникати:

  • file на кількох серверах - кожен сервер бачить лише свої сесії;
  • cookie для великих даних - ліміт розміру cookie близько 4 КБ і дані в кожному запиті;
  • array - лише для тестів.

Що не зберігати в сесії:

  • моделі й великі об'єкти - серіалізуються цілком, застарівають, роздувають кожен запит. Зберігайте ідентифікатори;
  • дані, що мають пережити вихід (кошик зареєстрованого користувача, налаштування) - у базі;
  • секрети й токени сторонніх сервісів без потреби: вміст сесії видно будь-кому з доступом до сховища (SESSION_ENCRYPT=true шифрує дані сесії);
  • дані, що змінюються паралельними запитами - без блокування вони перезаписують одне одного.

Ще кілька параметрів:

  • SESSION_LIFETIME - хвилини неактивності; для адмін-панелей коротше;
  • SESSION_CONNECTION - окреме з'єднання з базою чи Redis для сесій;
  • 'serialization' => 'json' у config/session.php замість PHP-серіалізації (за замовчуванням php) закриває клас атак через десеріалізацію, але вимагає зберігати в сесії лише прості значення.

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

paginate() має порахувати, скільки рядків поверне запит. Для простого запиту це select count(*) from ..., але зі складними запитами виникають дві проблеми.

1. GROUP BY чи HAVING. count(*) над запитом з групуванням повертає кількість у кожній групі, а не кількість груп. Laravel це враховує: якщо в запиті є groupBy чи having, він обгортає запит у підзапит:

select count(*) as aggregate from (
    select customer_id, sum(total) from orders group by customer_id
) as aggregate_table

Результат правильний, але база виконує весь запит з групуванням лише для підрахунку - на великих даних це повільно.

2. DISTINCT, об'єднання з дублями, UNION - підрахунок або дорогий, або без уважності дає неправильне число.

Варіанти розв'язання:

Не рахувати взагалі - simplePaginate() чи cursorPaginate(). Для більшості стрічок і звітів кнопок «далі/назад» достатньо.

Передати кількість самому - paginate() приймає параметр total:

$total = Cache::remember('reports:customers:count', 300, fn () => DB::table('orders')->distinct()->count('customer_id'));

$customers = DB::table('orders')
    ->select('customer_id', DB::raw('sum(total) as revenue'))
    ->groupBy('customer_id')
    ->orderByDesc('revenue')
    ->paginate(perPage: 50, total: $total);

Кількість груп можна порахувати простішим запитом (count(distinct customer_id)), закешувати чи взяти з денормалізованого лічильника.

Приблизна кількість: для «приблизно 1,2 млн результатів» - оцінка з EXPLAIN чи статистики таблиці (reltuples у PostgreSQL) замість точного count.

Перенести групування в окрему таблицю: для звітів, які відкривають часто, - матеріалізоване подання чи таблиця агрегатів, що оновлюється за розкладом. Тоді пагінація йде по простій таблиці з індексом.

Обмежити глибину: пошукові системи показують лише перші сотні результатів. Обмеження на кількість сторінок прибирає і дорогий підрахунок, і повільні далекі OFFSET.

Пастки з Eloquent:

  • paginate() з select і groupBy вимагає, щоб усі неагреговані колонки були в GROUP BY (PostgreSQL і MySQL з ONLY_FULL_GROUP_BY);
  • withCount і having разом з пагінацією - перевіряйте згенерований SQL через toRawSql();
  • порядок має бути детермінованим (orderBy('revenue')->orderBy('customer_id')), інакше записи з однаковим значенням перескакують між сторінками.

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

Питання з реальних технічних співбесід - 378 питань у 43 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.

Рівні
Junior 101 Middle 147 Senior 130

Готуєтесь до співбесіди не просто так: зараз на сайті 147 відкритих вакансій Laravel і PHP. Переглянути вакансії