Провайдер - єдине штатне місце для збирання застосунку: `register()` кладе привʼязки в контейнер, `boot()` викликається вже після того, як зареєструвалися всі провайдери. `bind` дає новий обʼєкт на кожне розвʼязання, `singleton` тримає один на весь життєвий цикл контейнера, а `scoped` - той самий singleton, який Octane і черговий воркер скидають між запитами та задачами.
Laravel для Middle: питання на співбесіду
14 питань рівня Middle з теми Laravel з розгорнутими відповідями, порадами та перевіркою.
У 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`.
Обсервер - це набір слухачів, підвішених до подій збереження конкретної моделі, тож він мовчить на будь-якій масовій операції через білдер і за замовчуванням виконується всередині транзакції, ще до коміту. Подія зі слухачем описує бізнес-факт, диспатчиться явно з одного місця й дочекається коміту, якщо реалізує `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()` віддає хвилину одному серверу з кластера, - а справжню гарантію дає ідемпотентність самої операції.