<? phpukraine СТАТТІ
Пошук по платформі
КАРʼЄРА 4 вересня 2026 · 7 хв читання

З Middle у Senior: що насправді відрізняє грейди на PHP-співбесідах

У банку питань порталу 52 картки: 13 junior, 25 middle, 14 senior. Якщо покласти senior-картки поруч із middle, різниця виявляється не в складності тем, а в тому, що питають: middle пояснює механіку, senior пояснює збій, називає ціну рішення і має критерії там, де правильної відповіді не існує.

РP
Редакція phpukraine
Редакція платформи

Банк питань порталу влаштований так: одне питання — одна картка з рівнем (junior, middle, senior), реальними формулюваннями «як питають», критеріями «що хоче почути інтервʼюер» і списком типових помилок. Зараз карток 52: 13 junior, 25 middle, 14 senior. Це достатній обсяг, щоб покласти senior-питання поруч із middle і подивитися, чим вони відрізняються по суті. Нижче — те, що з цього видно.

Senior-питань найменше, і жодне з них не «часто питають»

Позначку «часто питають» мають 23 картки з 52. Серед них немає жодної senior. Усі часті питання — junior і middle: життєвий цикл запиту в Laravel, service container, N+1, middleware, Form Request, == проти ===, declare(strict_types=1), читання EXPLAIN, INNER проти LEFT JOIN, actions і filters у WordPress, DI-контейнер Symfony.

Висновок неприємний для тих, хто готується до senior-співбесіди «по-senior»: базові питання нікуди не зникають. Розмова на senior починається з тих самих частих карток, просто на них дають менше часу й не пробачають плавання. Різниця не в тому, що вас перестають питати про eager loading. Різниця в тому, що після eager loading іде ще пʼять питань, яких middle не отримує взагалі.

Middle пояснює механіку, senior пояснює збій

Найпомітніша відмінність — навіть не тема, а формулювання. Ось як звучать питання в middle-картках:

  • «Чому сторінка зі списком робить 200 запитів до бази?»
  • «Що таке eager loading і коли він шкодить?»

А ось senior-картки:

  • «Чому під Octane один користувач бачить дані іншого?»
  • «Чому воркер черги росте в памʼяті, хоча кожен job невеликий?»
  • «Чому два паралельні запити списали з балансу більше, ніж там було?»
  • «Клієнт натиснув „Оплатити“ двічі: як не списати гроші двічі?»
  • «Debugbar каже 1.8 с, а користувач скаржиться на 8 с. Хто з них бреше?»

Middle-питання закривається описом механізму: розказав, як працює lazy loading, назвав with() — тема пройдена. Senior-питання описує симптом системи, яка вже в продакшені, і очікує, що ви розкладете його на складові й перевірите гіпотези по черзі. У картці про профілювання це прямо сформульовано як процедура: розкласти час (браузер → TTFB → PHP → база → зовнішні виклики), знайти найбільший доданок, змінити одну річ, виміряти ще раз. Оцінюють не назву профайлера, а наявність послідовності — і те, що ви не починаєте з гіпотези «мабуть, треба кеш».

Три теми, яких у middle майже немає

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

Одночасність. Рівні ізоляції, блокування, ідемпотентність. Ключова теза, за яку ставлять плюс: рівень ізоляції сам по собі не закриває втрачене оновлення. Патерн «прочитати в PHP, порахувати, записати» ламається на всьому, що нижче за SERIALIZABLE, а лікується він атомарним UPDATE з умовою або SELECT ... FOR UPDATE.

// Класична відповідь middle: транзакція. Вона тут не рятує —
// обидва процеси прочитали однаковий баланс і однаково його перезаписали.
$account = Account::find($id);
$account->balance -= $amount;
$account->save();

// Рішення приймає база, а не PHP: умова і віднімання в одному операторі
$updated = Account::whereKey($id)
    ->where('balance', '>=', $amount)
    ->update(['balance' => DB::raw('balance - '.(int) $amount)]);

if ($updated === 0) {
    // нуль оновлених рядків — це бізнес-відповідь «не вистачило коштів», а не збій
    throw new InsufficientFunds();
}

Продовження цього кластера — ідемпотентність: клієнтський ключ на одну бізнес-операцію, збережений разом із результатом під унікальним індексом, і розуміння, що перевірка if (! exists) у коді гонку не закриває, а доставка вебхуків і повідомлень черги гарантована щонайменше один раз, тобто повтори неминучі за визначенням.

Стан, який живе довше за один запит. Під php-fpm процес помирає після відповіді, і це прощає багато чого. Під Octane (Swoole, RoadRunner, FrankenPHP) чи в довгому воркері черги — ні.

// Під php-fpm обидва рядки поводяться однаково.
// Під Octane перший — це витік даних між користувачами:
// інстанс переживає запит разом із чужим Request усередині.
$this->app->singleton(CurrentTenant::class, fn ($app) => new CurrentTenant($app['request']));

// scoped — той самий біндінг, але контейнер скидає його на кожному запиті
$this->app->scoped(CurrentTenant::class, fn ($app) => new CurrentTenant($app['request']));

Сюди ж — три джерела витоку стану, які має назвати senior: синглтони в контейнері, статичні властивості класів і memoization у хелперах або глобальних змінних. І механіка памʼяті: PHP звільняє обʼєкт миттєво, щойно refcount падає до нуля, а окремий збирач циклів запускається не за таймером, а за заповненням root buffer (10 000 можливих коренів, поріг адаптивний із PHP 7.3). Тому «воркер росте в памʼяті» — це майже завжди накопичений статичний масив або нескинутий реєстр слухачів, а не проблема GC.

Зміна схеми під живим трафіком. Expand and contract, і розуміння, що під час деплою стара й нова версія коду одночасно працюють з однією базою. Перейменування колонки без простою — це чотири релізи: нова nullable-колонка, подвійний запис плюс backfill батчами, перемикання читання, видалення старої. Плюс знання, який DDL блокує таблицю: NOT NULL без default на великій таблиці, зміна типу, індекс без CONCURRENTLY у PostgreSQL. І окреме питання, на якому валяться навіть сильні кандидати: де запускаються міграції — окремим кроком до перемикання трафіку, а не в entrypoint кожної репліки.

Ціна рішення — обовʼязкова частина senior-відповіді

Це найчіткіший маркер грейду в усьому банку. У senior-картках критерії «що хоче почути інтервʼюер» майже завжди містять межу застосовності:

  • Octane нічого не компілює і не робить ваш код швидшим — він прибирає бутстрап фреймворку з кожного запиту, і за це ви платите станом, який більше не скидається сам.
  • Черга нічого не пришвидшує — вона переносить час на воркер і додає вам стан «в обробці» в UI, ідемпотентність, tries, backoff, failed_jobs і моніторинг довжини черги.
  • Кеш навколо повільного місця — не діагностика: після інвалідації, деплою і кожного нового фільтра ті самі чотири секунди повертаються.
  • Картка «Коли DDD виправданий» прямо вимагає прикладу, де ви свідомо DDD не застосували, і чому анемічна модель із транзакційним скриптом там була правильним вибором.

Middle-картки такого не просять — там достатньо знати, що робить with() і чим від нього відрізняється load(). Senior-відповідь без ціни звучить як реклама технології, і саме на цьому валяться кандидати, які вивчили нову річ, але ще не платили за неї в продакшені.

Питання, у яких немає правильної відповіді

Окремий клас senior-питань — архітектурний вибір: Livewire чи API з SPA, дії й модулі чи стандартна структура, репозиторій над Eloquent чи ні. Тут інтервʼюер не тримає в голові еталонну відповідь; він перевіряє, чи є у вас критерії.

Найгірша відповідь — «залежить» без продовження. Робоча — назвати осі рішення вголос і пройтися по них. Для фронтенду: чи існує другий клієнт (якщо так, API вже в кошторисі й SPA дешевшає); чи є взаємодії частіші за клік (вони не переживуть round-trip); хто чергує вночі (команда без фронтенд-компетенції платитиме за SPA щодня). Для структури проєкту критерій ще коротший: у кожного бізнес-сценарію має бути рівно одне місце, куди по нього приходять — а далі сходинки від «контролер плюс модель» до модуля з власним провайдером і арх-тестом.

Показова пара карток — Form Request і DTO. Junior-рівень: як винести валідацію з контролера. Senior-рівень: чому App\Http\Requests\... не можна тягнути в джобу чи консольну команду.

// Контракт сценарію без Illuminate: його однаково будує контролер,
// artisan-команда і споживач черги — сценарій лишається викликуваним поза HTTP.
final readonly class NewSubscription
{
    public function __construct(
        public int $customerId,
        public string $planCode,
        public ?string $promoCode = null,
    ) {}
}

Як цим користуватись у підготовці

  • Пройдіть middle-підбірку на швидкість. Якщо на частому питанні ви думаєте більше пів хвилини, це діра, яку помітять першою.
  • Візьміть senior-підбірку і для кожної картки сформулюйте не лише відповідь, а й ціну рішення та випадок, коли ви його свідомо не застосували.
  • Для трьох senior-кластерів — одночасність, стан у довгих процесах, міграції під трафіком — підготуйте по одному реальному випадку з власної практики. Саме їх просять картки про ізоляцію транзакцій, GC і Octane.
  • Не міняйте базу на «senior-теми»: 23 з 52 карток позначені як часті, і senior серед них немає жодної.
ПИШЕТЕ ПРО PHP?Опублікуйте розбір або історію з проєкту на платформіРедактор із чеклістом, редактура, авторська сторінка. Републікація з блогу отримує canonical на оригінал. Відкрити редактор →
РP
Редакція phpukraine
Редакція платформи
Матеріали, які готує команда платформи на основі власних даних: каталогу вакансій, зарплатного звіту й банку питань. Кожна цифра в них рахується з бази, а не береться з голови.
оновлено 4 вересня 2026 · ліцензія CC-BY-SA-4.0
ДАЛІ ПО ТЕМІ
ЧИТАТИ ДАЛІ
← Усі статті