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

Питання на співбесіді рівня Senior

Найпопулярніші питання з реальних Laravel/PHP співбесід для всіх рівнів

62 питань

Telescope - інструмент дебагу/спостереження. Збирає й показує у дашборді: запити, винятки, SQL-запити (з часом і дублями), завдання черг, листи, нотифікації, кеш, події, HTTP-клієнт.

php artisan telescope:install
  • Незамінний для пошуку N+1 (бачиш усі запити сторінки), повільних місць, помилок у чергах.
  • Дані зберігаються в БД; у проді обмежують доступ через gate viewTelescope та вмикають sampling, бо обсяг записів великий.

На відміну від Horizon (керування чергами), Telescope - про діагностику всього застосунку. У продакшені часто доповнюють зовнішнім APM (Sentry, Datadog).

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

У кластері з кількох інстансів локальний лічильник не годиться - потрібен спільний стан у Redis. Фасад RateLimiter під капотом використовує атомарні операції Redis (INCR + EXPIRE), тож підрахунок точний між усіма серверами.

RateLimiter::attempt(
    key: 'send-sms:'.$user->id,
    maxAttempts: 5,
    callback: fn () => $this->sendSms(),
    decaySeconds: 60,
);

Для складніших схем - алгоритми sliding window чи token bucket на Lua-скриптах (атомарність на стороні Redis, без гонок). Ключове: лічильник і його TTL мають змінюватися атомарно, інакше за конкурентного доступу ліміт «протікає».

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

Circuit Breaker захищає від каскадних збоїв при зверненні до ненадійної залежності (зовнішнє API, що «лежить»). Якщо помилок забагато - «ланцюг розривається», і запити певний час відхиляються миттєво, не витрачаючи ресурси на марні спроби.

Стани:

  • Closed - усе працює, запити йдуть.
  • Open - поріг помилок перевищено; запити одразу падають (fail fast).
  • Half-Open - через таймаут пропускаються пробні запити; успіх → Closed, провал → знову Open.
// концептуально через Cache як лічильник збоїв
if (Cache::get('cb:payments') === 'open') {
    throw new ServiceUnavailableException;
}

У Laravel реалізують через лічильники в Redis/Cache або пакети-обгортки HTTP-клієнта. Часто поєднують із retry + backoff.

CI автоматично перевіряє кожен пуш, CD - автоматично доставляє код.

Типовий пайплайн (GitHub Actions / GitLab CI):

- composer install
- vendor/bin/pint --test # стиль
- vendor/bin/phpstan analyse # статичний аналіз
- php artisan test --parallel # тести
- npm ci && npm run build # ассети
# → деплой при успіху

Деплой: SSH-скрипт, Docker-образ у реєстрі + rolling update у Kubernetes, або сервіси на кшталт Forge/Envoyer. На етапі деплою - migrate --force, кешування конфіга/маршрутів, queue:restart. CI/CD дає швидкий зворотний зв'язок і знижує ризик людської помилки при релізі.

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

  • Stateful - сервер зберігає стан клієнта між запитами (наприклад, сесія у файлі/пам'яті конкретного інстансу). Тоді потрібна «липкість» (sticky sessions) або спільне сховище.
  • Stateless - сервер не зберігає стану; кожен запит самодостатній і містить усе потрібне (наприклад, JWT/токен з даними автентифікації).
Stateful:  сесія на сервері  → потрібен спільний Redis/sticky LB
Stateless: токен у запиті     → будь-який інстанс обробить запит

Stateless легше масштабувати горизонтально - інстанси взаємозамінні. У Laravel веб-частина зазвичай stateful (сесії в Redis), API - stateless (Sanctum-токени). Для масштабування цей стан виносять у спільні Redis/БД.

Sharding - горизонтальне розбиття даних між кількома незалежними БД (шардами) за ключем (tenant_id, user_id, geo). Дозволяє вийти за межі одного сервера.

shard 1: користувачі 1–1M
shard 2: користувачі 1M–2M
  • Shard key обирають так, щоб дані рівномірно розподілялись і більшість запитів влучали в один шард.
  • Складнощі: запити між шардами, унікальність ID (часто UUID/Snowflake), ребалансування при додаванні шарда, відсутність крос-шардових операцій JOIN і транзакцій.

У Laravel реалізують через множинні з'єднання й маршрутизацію за shard key. Sharding - крайній захід, коли вертикальне масштабування та репліки вже не справляються.

Версіонування дозволяє розвивати API, не ламаючи наявних клієнтів. Стратегії:

URI versioning (найпоширеніше) - версія в шляху:

Route::prefix('v1')->group(base_path('routes/api_v1.php'));
Route::prefix('v2')->group(base_path('routes/api_v2.php'));

Header/Media-type versioning - Accept: application/vnd.app.v2+json. Чистіші URL, але складніше тестувати.

Практики:

  • Окремі неймспейси контролерів і API Resources на версію (V1\PostResource, V2\PostResource).
  • Бізнес-логіку виносити в спільні Action/Service, щоб не дублювати між версіями.
  • Політика deprecation: підтримувати стару версію певний строк, повертати заголовки Deprecation/Sunset.

Докладніше в документації: API Resources (версіонування)

Deadlock - дві транзакції взаємно блокують одна одну, чекаючи на ресурси, які тримає інша. СУБД виявляє це й «вбиває» одну з транзакцій.

Запобігання:

  • Єдиний порядок доступу до таблиць/рядків у всіх транзакціях.
  • Тримати транзакції короткими, блокувати якомога пізніше.
  • Правильні рівні ізоляції (не завищувати без потреби).

Обробка в Laravel - автоматичний повтор:

DB::transaction(function () {
    // ...
}, attempts: 3); // повторити при deadlock

Діагностика: SHOW ENGINE INNODB STATUS (MySQL), логи БД, моніторинг частоти deadlock. Інколи допомагає optimistic locking замість тривалих блокувань.

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

Serverless виконує код без керування серверами: провайдер (AWS Lambda) сам масштабує під навантаження й тарифікує за фактичні виклики.

Laravel Vapor - платформа для деплою Laravel на AWS Lambda + API Gateway, з керованими БД, чергами (SQS), кешем і CDN.

Особливості й обмеження:

  • Авто-масштабування до нуля й під пік; платиш лише за використання.
  • Cold start - затримка першого запиту після простою.
  • Файлова система ефемерна → файли лише в S3.
  • Обмеження часу виконання Lambda → довгі завданьі в черги.
  • Stateless за дизайном (сесії/кеш у Redis/DynamoDB).

Альтернатива - контейнери (ECS/Kubernetes), коли потрібен повний контроль або стабільні довготривалі процеси.

WebSocket - постійне двостороннє з'єднання поверх одного TCP, що дає реальний час без поллінгу.

У Laravel сервер WebSockets - Reverb (офіційний), Soketi або Pusher; події транслюються через Broadcasting, клієнт слухає через Echo.

Масштабування: коли інстансів WebSocket-сервера кілька, клієнти на різних інстансах не «бачать» одне одного. Рішення - Redis Pub/Sub як спільна шина: інстанс публікує повідомлення в Redis, усі інстанси отримують і розсилають своїм підключеним клієнтам.

client A ─ inst 1 ┐
                  ├─ Redis Pub/Sub ─┤
client B ─ inst 2 ┘

Окрема увага: авторизація private/presence-каналів, ліміти відкритих з'єднань, sticky sessions на балансувальнику.

Докладніше в документації: Broadcasting / WebSockets

2FA вимагає двох факторів: «що знаю» (пароль) + «що маю» (код із застосунку/SMS). Найпоширеніше - TOTP (Time-based One-Time Password) сумісно з Google Authenticator.

У Laravel найпростіше через Fortify, який має 2FA з коробки:

  • генерація секрету й QR-коду для прив'язки;
  • перевірка 6-значного коду при вході;
  • одноразові recovery codes на випадок втрати пристрою.
// Fortify вмикає features:
Features::twoFactorAuthentication(['confirm' => true]),

Під капотом - пакет pragmarx/google2fa. Важливо: зберігати секрет зашифрованим, давати recovery-коди, за бажанням «запам'ятати пристрій».

Докладніше в документації: Двофакторна автентифікація (Fortify)

Колекції зручні настільки, що ними легко почати робити роботу бази - і заплатити памʼяттю та часом.

Типова помилка:

$active = Post::all()->where('is_published', true);        // вся таблиця в памʼять
$count  = Post::all()->count();                            // теж уся таблиця
$latest = Post::all()->sortByDesc('created_at')->take(5);  // і сортування в PHP

Кожен рядок витягує всі записи, щоб потім відкинути більшість.

Те саме в базі:

$active = Post::where('is_published', true)->get();
$count  = Post::count();
$latest = Post::latest()->limit(5)->get();

Різниця не косметична: база фільтрує за індексом і повертає стільки, скільки потрібно.

Як розрізняти. Методи Builder виконуються в SQL, методи Collection - у PHP. Межа проходить там, де стоїть get(), all() чи first(): усе після них - уже PHP.

Плутанину створює однакова назва: where() є і в білдера, і в колекції. Post::where(...) фільтрує в базі, Post::all()->where(...) - у памʼяті.

Коли колекція доречна:

  • Дані вже отримані, і потрібне ще одне групування чи перетворення - другий запит дорожчий.
  • Логіка не виражається в SQL: складна умова на PHP, робота з обʼєктами-значеннями.
  • Набір свідомо малий - десятки рядків, де різниці немає.

Коли ні: фільтрація, підрахунок, сортування й обмеження на таблиці, розмір якої росте. Це робота бази.

Проміжний варіант - lazy(): обхід пачками з синтаксисом колекції, коли обробити треба всі рядки, але тримати їх у памʼяті не можна.

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

Laravel Scout - драйверна абстракція повнотекстового пошуку. Додаєте трейт до моделі - і вона автоматично синхронізується з пошуковим індексом (Meilisearch, Algolia, Elasticsearch, навіть database).

class Post extends Model
{
    use Searchable;

    public function toSearchableArray(): array
    {
        return ['title' => $this->title, 'body' => $this->body];
    }
}

Post::search('laravel queues')->paginate(15);
  • Індекс оновлюється на події моделі (краще - через чергу).
  • Початкове наповнення: php artisan scout:import "App\Models\Post".
  • Meilisearch дає швидкий typo-tolerant пошук «з коробки»; Elasticsearch - складніші аналітичні запити.

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

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

П'ять місць, де це видно:

1. Побічні ефекти не відкочуються. Лист, відправлений усередині транзакції, піде навіть після rollback - база відкотиться, а користувач уже отримав «Замовлення оформлено».

2. Завдання стартує раніше за commit. Воркер може взяти завдання до завершення транзакції й не знайти запису. Лікується afterCommit() на завданні або ShouldDispatchAfterCommit на події:

ProcessOrder::dispatch($order)->afterCommit();

3. Вкладені транзакції - не транзакції. Друга DB::transaction() усередині першої не створює окрему: використовуються savepoint-и, і зовнішній rollback скасує все одно все.

4. DDL не відкочується в MySQL. Schema::create() усередині транзакції робить неявний commit - міграції на MySQL атомарними не бувають, на відміну від PostgreSQL.

5. Довга транзакція тримає блокування. Звернення до зовнішнього API всередині транзакції означає, що рядки заблоковані на весь час мережевого очікування.

Практичне правило: усередині транзакції - лише запити до бази. Листи, черги, HTTP - після неї, за фактом успіху.

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

Класична гонка: два процеси читають баланс 100, кожен додає 50, обидва пишуть 150 - одне списання зникло. Читання й запис мають бути однією неподільною операцією.

Атомарний вираз - найдешевше, коли значення рахується з поточного:

Account::whereKey($id)->increment('balance', 50);
// UPDATE accounts SET balance = balance + 50 WHERE id = ?

База сама рахує нове значення, читати наперед не потрібно.

Песимістичне блокування - коли між читанням і записом є логіка:

DB::transaction(function () use ($id) {
    $account = Account::whereKey($id)->lockForUpdate()->first();

    if ($account->balance < 50) {
        throw new InsufficientFunds;
    }

    $account->decrement('balance', 50);
});

lockForUpdate() тримає рядок до кінця транзакції - інші процеси чекають. Обовʼязково всередині DB::transaction(), інакше блокування знімається одразу. Є ще sharedLock() - дозволяє читати, забороняє змінювати.

Оптимістичне блокування - без блокувань, через версію рядка:

$updated = Account::whereKey($id)
    ->where('version', $account->version)
    ->update(['balance' => $new, 'version' => $account->version + 1]);

if ($updated === 0) {
    // хтось випередив - перечитати й повторити
}

Дешевше під навантаженням, але вимагає обробки повтору.

Поза базою - Cache::lock() для операцій, що зачіпають не тільки БД:

Cache::lock('import:'.$id, 10)->block(5, function () {
    // виконується лише в одному процесі
});

Що обирати: просте збільшення - атомарний вираз; логіка між читанням і записом - lockForUpdate(); висока конкуренція за рідкісні конфлікти - оптимістичне.

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

Вакансії Laravel рівня Senior

Усі вакансії Laravel Senior
Mantah Нова
Сьогодні

Backend Developer (PHP)

Backend розробник для e-commerce проектів та корпоративних порталів. Розробляє архітектуру, обирає стек технологій, розвиває та підтримує PHP-системи, інтегрує зовнішні сервіси (платежі, CRM/ERP, маркетплейси). Вимоги: 3+ років досвіду, PHP 8.x, Laravel/Symfony, Magento 2, WordPress/WooCommerce, REST API/GraphQL, MySQL, Redis, Git/Docker, Upper-Intermediate англійська.

Real Estate Bees Нова
Сьогодні

Backend Engineer (PHP/Laravel)

Розробник Backend на Laravel 12/PHP 8.2 для revenue-critical систем з REST API, бізнес-логікою, інтеграцією третіх сервісів і Redis-черг. Потрібен досвід 3+ років PHP/Laravel, знання Domain-Driven Design, PostgreSQL, тестування та AI-інструментів. Позиція передбачає архітектурну трансформацію з legacy-коду до domain-first структури, з відповідальністю від вимог до production.

Edvantis
4 дні тому

Senior Full-Stack Software Engineer (PHP/Laravel, Vue.js)

Розробник повного стеку для хмарної платформи управління дослідженнями. Розроблення функцій на PHP (Laravel) та Vue.js, побудова REST API, оптимізація продуктивності, написання тестів. Вимоги: міцні знання PHP та Laravel, досвід з Vue.js і JavaScript, англійська Upper-Intermediate+.

Питання рівня Senior з реальних співбесід Laravel і PHP - 62 питання у 33 темах, розібраних із відповідями. Нижче - теми цього рівня та сусідні рівні, якщо хочете звузити або розширити підготовку.

Інші рівні
Junior 52 Middle 57