Eloquent і Query Builder генерують той самий SQL, різниця починається після того, як рядки прийшли: `get()` на моделі створює обʼєкт на рядок, копіює атрибути в `$original` і тримає масив `$relations`, а базовий білдер віддає `stdClass`. Для звітів і вивантажень беруть `toBase()` або `DB::table()`, агрегують у SQL, а великі набори проганяють через `chunkById()`/`lazyById()`, не через `chunk()`.
Питання на співбесіду Laravel
Питання для Junior, Middle та Senior Laravel-розробників: service container, Eloquent і N+1, черги, події, кеш, тестування та архітектура великих застосунків.
Фасад це статичний фасад над контейнером: `__callStatic` бере обʼєкт із контейнера за рядковим акцесором і викликає на ньому звичайний метод, тому `swap()`, `shouldReceive()` і всі `fake()` працюють. У Laravel-склейці (контролери, middleware, роути, Blade) фасад економить друкування, а в доменному коді залежності мають бути в конструкторі, бо саме сигнатура класу є списком того, що йому потрібно.
Scope - це іменований шматок запиту: локальний ви викликаєте самі (`Post::published()`), глобальний Eloquent додає в кожен запит моделі, як `SoftDeletes`. Accessor через `Attribute::get()` і cast працюють уже після вибірки, у PHP: каст перетворює значення колонки в обидва боки, аксесор рахує похідний атрибут, якого в таблиці немає, тому фільтрувати й сортувати за ним не можна, а `$appends` додає його в `toArray()` і `toJson()`.
Контейнер відповідає за створення обʼєктів і підстановку залежностей: читає типи параметрів конструктора через рефлексію і рекурсивно створює їх.
Обидва шляхи запускають один і той самий Validator: `$request->validate()` робить це всередині методу контролера, Form Request — під час резолву аргументів, ще до входу в метод, і додає authorize(), prepareForValidation() і власні повідомлення. Інлайн — для двох-трьох простих правил, Form Request — щойно зʼявляються авторизація, нормалізація вхідних даних або повторне використання правил.
Роутер зіставляє URI з маршрутами в порядку реєстрації, а перетворення `{post}` на об'єкт робить не контролер, а middleware `SubstituteBindings`: воно бере `getRouteKeyName()`, робить `firstOrFail()` і кидає 404 ще до вашого коду. Неявне зв'язування шукає по всій таблиці й ігнорує ієрархію URL, поки не ввімкнути `scopeBindings()`, а `SoftDeletes` мовчки вирізає видалені записи, доки на маршруті не стоїть `withTrashed()`.
public/index.php підключає автолоадер і bootstrap/app.php, зібраний застосунок віддає запит HTTP-ядру: воно виконує бутстрапери (env, config, фасади, провайдери), проганяє запит через глобальні middleware, роутер знаходить маршрут, запускає його middleware й контролер, а відповідь повертається тим самим ланцюгом назад до send() і terminate().
Тип звʼязку визначає не «логіка», а те, де лежить ключ: hasOne/hasMany — ключ у дочірній таблиці, belongsTo — у поточній, belongsToMany — у проміжній, morph* — пара колонок *_id і *_type, коли дитина належить кільком типам батьків.
Провайдер - єдине штатне місце для збирання застосунку: `register()` кладе привʼязки в контейнер, `boot()` викликається вже після того, як зареєструвалися всі провайдери. `bind` дає новий обʼєкт на кожне розвʼязання, `singleton` тримає один на весь життєвий цикл контейнера, а `scoped` - той самий singleton, який Octane і черговий воркер скидають між запитами та задачами.
У Laravel «unit» і «feature» — це не «клас проти роуту», а «без завантаженого застосунку проти з ним»: у стандартному `tests/Pest.php` `Tests\TestCase` підключається лише до теки `Feature`, тому в `Unit` немає ні контейнера, ні фасадів, ні бази. Логіку без Illuminate тестують швидкими unit-тестами, все інше — feature-тестами через HTTP-межу з фабриками для даних і фейками (`Queue`, `Mail`, `Http`, `Storage`) для зовнішніх меж; фейк доводить лише факт виклику, тому job чи mailable потребують власного тесту.
Guard відповідає на питання «як пізнати користувача в цьому запиті» (session, token, sanctum), provider - «де його взяти» (eloquent-модель або таблиця через `database`). Sanctum закриває обидва типові випадки: персональні токени в `personal_access_tokens` для мобільних і сторонніх скриптів та звичайну cookie-сесію для свого SPA на тому ж домені. Passport потрібен лише тоді, коли ви самі стаєте OAuth2-провайдером із клієнтами, екраном згоди й refresh-токенами, а готовий фронтенд для входу беруть зі стартових наборів Laravel 12 (React, Vue, Livewire), бо Breeze і Jetstream уже не той шлях, який радить документація.
Усі чотири механізми - це один `Gate`: `Gate::authorize()` шукає policy за першим аргументом (`App\Models\Post` -> `App\Policies\PostPolicy`), інакше падає на іменовану здібність з `Gate::define()`, а `can` middleware, `$this->authorize()`, `authorize()` у Form Request і `@can` у Blade лише викликають його в різних точках запиту. Відмова кидає `AuthorizationException` -> 403, а `Response::denyAsNotFound()` перетворює її на 404, коли сам факт існування запису розголошувати не можна.
N+1 виникає, коли для колекції з N записів виконується ще N запитів на звʼязки; виправляється eager loading, а ловиться через preventLazyLoading, Debugbar або Telescope.
Кількість спроб задають `$tries` (або `$maxExceptions`), паузу між ними — `$backoff` чи метод `backoff()`, дедлайн — `retryUntil()`, а фінальний обробник — `failed(Throwable $e)`. Ключова пастка не в цих властивостях, а в тому, що `retry_after` у config/queue.php має бути більшим за `$timeout` job, інакше воркер підхопить ще працюючий job і виконає його вдруге. Horizon дає ті самі налаштування на рівні супервізора, дашборд і теги; `ShouldBeUnique`, `Bus::batch()` і `Bus::chain()` керують не повторами, а тим, що взагалі потрапить у чергу і в якому порядку.
Middleware — це шари Pipeline навколо контролера: код до `$next($request)` бачить запит, код після — уже готову відповідь. Спершу йдуть глобальні (у порядку реєстрації, ще до роутера), потім групові й маршрутні — але ці фреймворк переставляє за списком пріоритетів, а `terminate()` викликається окремо, вже після відправки відповіді.
Таблиця `migrations` зберігає лише імена вже виконаних файлів, тому правка старої міграції на проді не виконується ніколи і дає розʼїзд схеми між середовищами: зміну оформлюють новим файлом. Без простою це три окремі кроки в різних релізах - nullable-колонка, бекфіл порціями поза міграцією, і аж потім NOT NULL через `CHECK ... NOT VALID` та індекс `->online()` у міграції з `$withinTransaction = false`.
Laravel 11 прибрав з нового скелета `Http/Kernel`, `Console/Kernel`, `Exceptions/Handler`, половину конфігів і теку `lang`, перенісши налаштування в `bootstrap/app.php` і в дефолти фреймворка; 12 і 13 структуру не чіпали, тож оновлення між ними - це бамп версій у `composer.json` плюс вичитаний `UPGRADE.md`, а не переписування застосунку.
`Collection` - це обгортка над масивом, який уже цілком у памʼяті, тож кожен `map()`/`filter()`/`groupBy()` робить повний прохід і створює новий масив, а `Model::all()->where(...)` спершу читає всю таблицю. Фільтрувати, рахувати й групувати має база; `LazyCollection` через `cursor()`, `lazy()` і `lazyById()` потрібна тоді, коли рядків більше, ніж влазить у памʼять.
Компонент - це ізольований шматок розмітки з явним контрактом: `@props` (або конструктор класу) описують очікувані дані й забирають ці ключі з `$attributes`, `$slot` приймає вкладений вміст, а решта переданих атрибутів осідає в `ComponentAttributeBag` і виводиться через `$attributes->merge()`, де `class` склеюється, а інші атрибути перекриваються викликом. `@include` навпаки не має ні контракту, ні слотів: він просто вставляє файл, якому видно всі змінні батьківської сторінки.
Обсервер - це набір слухачів, підвішених до подій збереження конкретної моделі, тож він мовчить на будь-якій масовій операції через білдер і за замовчуванням виконується всередині транзакції, ще до коміту. Подія зі слухачем описує бізнес-факт, диспатчиться явно з одного місця й дочекається коміту, якщо реалізує `ShouldDispatchAfterCommit`.
Event описує факт, що щось сталося; listener реагує на нього; job — це одиниця відкладеної роботи в черзі, яку хтось явно поставив.
Модель серіалізується через `toArray()`, тож форма відповіді — це набір колонок таблиці плюс випадково завантажені звʼязки й `$appends`: нова колонка публікується сама, а `$hidden` захищає лише те, що ви згадали. `JsonResource` — це білий список полів у явному класі, де `whenLoaded()` прибирає ключ замість ліниво тягнути звʼязок, а `Resource::collection($paginator)` сам додає `links` і `meta`.
Кеш стає застарілим з двох причин: ключ не враховує всього, від чого залежить результат, або інвалідація не спрацювала (масовий update без подій моделі, forget усередині ще не закомміченої транзакції). Лікується не меншим TTL, а тегами й обсерверами з `$afterCommit`, а одночасний промах гарячого ключа — `Cache::lock()` або `Cache::flexible()`.
DB::transaction($callback, $attempts) обгортає замикання в транзакцію, відкочує її на будь-якому Throwable і повторює лише при deadlock чи lock wait timeout; lockForUpdate() потрібен там, де ви читаєте значення, щоб на його основі писати, і блокування тримається до COMMIT — тому має сенс тільки всередині транзакції.
Розклад живе в коді, а cron має один рядок `* * * * * php artisan schedule:run`: цей процес щохвилини порівнює cron-вираз кожної задачі з поточною хвилиною і запускає лише ті, що настали. Від дублів захищають два різні локи в кеші - `withoutOverlapping()` не дає задачі наздогнати саму себе на одному вузлі, `onOneServer()` віддає хвилину одному серверу з кластера, - а справжню гарантію дає ідемпотентність самої операції.
Вибір визначають три речі, а не смак: чи є другий клієнт (мобілка, партнери) — тоді API все одно доведеться зробити, і SPA стає майже безкоштовним; яка зернистість взаємодій — Livewire платить мережевим round-trip за кожен крок, тому годиться для форм, таблиць і фільтрів, а не для перетягування, канви чи офлайну; і хто це супроводжуватиме. Livewire тримає стан на сервері й ганяє снапшот компонента туди-сюди, SPA тримає стан у браузері й вимагає версіонованого контракту; Inertia — середина, де є клієнтські компоненти, але немає публічного API.
Octane тримає застосунок у памʼяті між запитами через Swoole, FrankenPHP або RoadRunner: бутстрап відбувається один раз, але стан більше не скидається сам.
Спершу розкласти час: браузерний waterfall відділяє TTFB від фронтенду, а далі Debugbar/Clockwork/Telescope локально й Blackfire або Pulse у проді показують, куди йдуть мілісекунди всередині PHP. Оптимізують за спаданням внеску: кількість запитів (N+1) → важкі запити з `EXPLAIN` → синхронні зовнішні виклики в чергу → гігієна деплою (`opcache`, `php artisan optimize`, `composer -o`), і лише потім кеш і Octane.
Стандартної структури вистачає, доки логіка сценарію вміщається в контролер плюс модель; далі вузьке місце — не назви тек, а те, що бізнес-правило не має одного власника. Дія з одним публічним методом і незмінним DTO на вході дає цього власника без DDD, а тактичні патерни DDD додають лише там, де є інваріанти, які треба захищати, і мова, якою говорить бізнес.
Колонка `tenant_id` дає спільні міграції та один пул зʼєднань, але ізоляція живе в кожному `WHERE`, і будь-який забутий фільтр стає витоком. База (або схема) на тенанта переносить межу на рівень конекшна ціною N міграцій, N бекапів і пулу зʼєднань. Глобальний scope закриває лише читання через Eloquent: `DB::table()`, правила `unique`/`exists` і відновлення моделі в черзі його не бачать, а кеш, файли, сесії та scheduler треба розводити по тенантах окремо.
На Laravel-позиціях майже завжди питають service container і N+1: два маркери, за якими відрізняють Middle від Junior. Senior-рівень додає черги, ідемпотентність і межі домену.