Питання на співбесіді: Архітектура
Найпопулярніші питання з реальних Laravel/PHP співбесід для всіх рівнів
12 питань
Фасади надають зручний статичний інтерфейс до об'єктів із Service Container.
Cache::put('key', 'value', 60);
Route::get('/', fn () => view('home'));
Попри статичний синтаксис, це не справжні статичні методи: фасад через __callStatic() дістає реальний об'єкт із контейнера й викликає метод уже на ньому. Тому фасади тестовані - їх можна мокати:
Cache::shouldReceive('get')->once()->andReturn('value');
Кожен фасад має «accessor» - рядковий ключ сервісу в контейнері.
Repository Pattern абстрагує доступ до даних за інтерфейсом, ховаючи конкретну реалізацію (Eloquent, API, файли).
interface PostRepository
{
public function published(): Collection;
}
Нюанс для Laravel: Eloquent уже є реалізацією Active Record і сам по собі є абстракцією над БД. Тому додавання репозиторіїв часто надлишкове й лише дублює API Eloquent. Виправдане, коли:
- треба підміняти джерело даних (Eloquent ↔ зовнішнє API);
- є складна доменна логіка (DDD), де моделі не мають знати про БД.
У типовому CRUD-застосунку спільнота вважає це антипатерном.
Шлях запиту:
public/index.php- єдина точка входу; підключає автозавантажувач Composer.- Створюється екземпляр застосунку (Service Container) із
bootstrap/app.php. - HTTP Kernel обробляє запит, завантажує Service Providers (
register→boot). - Запит проходить глобальні middleware (наприклад, обробка сесій, CSRF).
- Router зіставляє URL із маршрутом, виконуються middleware маршруту.
- Викликається контролер/замикання, формується Response.
- Відповідь проходить middleware у зворотному порядку й повертається клієнту; виконується
terminate().
Ключова ідея: контейнер і провайдери бутстрапять застосунок, а middleware утворюють «цибулю» навколо обробки запиту.
- CQRS (Command Query Responsibility Segregation) розділяє запис (Commands, що змінюють стан) і читання (Queries). Read-модель можна оптимізувати окремо (денормалізовані проєкції, окрема БД).
- Event Sourcing зберігає не поточний стан, а послідовність подій; поточний стан відновлюється їх відтворенням. Дає повний аудит і «подорож у часі».
// концептуально
$aggregate->retrieve($uuid)
->placeOrder($data) // emit OrderPlaced
->persist(); // зберегти подію
У Laravel зазвичай через пакет spatie/laravel-event-sourcing (aggregates, projectors, reactors). Застосовувати варто там, де критичні аудит і складна доменна логіка - це додає суттєву складність, тож не для типового CRUD.
Два основні підходи:
Single Database (shared schema) - усі орендарі в одній БД, розділення за tenant_id у кожній таблиці. Ізоляція забезпечується global scope, що автоматично додає where tenant_id = ?.
- Плюси: просто й дешево. Мінуси: ризик витоку даних при помилці у scope.
Multi Database - окрема БД (або схема) на орендаря, динамічне перемикання з'єднання за поточним tenant.
- Плюси: сильна ізоляція, легше бекапити/масштабувати окремого клієнта. Мінуси: складніші міграції (на кожну БД).
Tenancy::initialize($tenant); // перемкнути конфіг з'єднання/кеш/файли
Популярний пакет - stancl/tenancy. Вибір залежить від вимог до ізоляції та масштабу.
Гексагональна архітектура ізолює ядро бізнес-логіки від зовнішнього світу.
- Ports - інтерфейси, через які ядро спілкується зі світом (
PaymentGateway,UserRepository). - Adapters - конкретні реалізації портів (
StripeAdapter,EloquentUserRepository, HTTP-контролер).
[ HTTP / CLI / Queue ] → Port → [ Domain Core ] → Port → [ DB / API / Mail ]
(adapters) (бізнес-логіка) (adapters)
Ядро не знає про Laravel, БД чи HTTP - воно залежить лише від абстракцій. Перевага: домен тестується ізольовано, зовнішні залежності легко підмінювати. У Laravel порти біндять до адаптерів через Service Container.
DDD фокусується на моделюванні бізнес-домену спільною мовою з експертами. Ключові поняття: Entities, Value Objects, Aggregates, Domain Events, Bounded Contexts.
У Laravel це зазвичай означає відхід від стандартної структури (app/Models, app/Http) на користь організації за доменами:
app/Domain/Ordering/
Models/Order.php
Actions/PlaceOrder.php
ValueObjects/Money.php
Events/OrderPlaced.php
- Бізнес-логіка живе в домені, а не в контролерах чи моделях-«божках».
- Контролери стають тонкими адаптерами, що викликають доменні дії.
DDD виправданий у складних доменах; для CRUD він додає зайвий оверхед.
П'ять принципів ООП-дизайну. У Laravel вони реалізуються природно завдяки сервіс-контейнеру.
S - Single Responsibility: клас має одну причину для зміни. На практиці - виносити бізнес-логіку з «товстих» контролерів у Service/Action-класи, валідацію - у Form Requests, логіку життєвого циклу моделі - в Observers.
O - Open/Closed: відкритий для розширення, закритий для модифікації. Приклад - драйвери Laravel (cache, queue, filesystem): новий драйвер додається через extend(), не змінюючи ядро.
L - Liskov Substitution: реалізації взаємозамінні через спільний інтерфейс без поломки логіки - напр., будь-який драйвер кешу можна підставити замість іншого.
I - Interface Segregation: багато вузьких інтерфейсів краще за один «товстий»; клас не має реалізовувати методи, які не використовує.
D - Dependency Inversion: залежати від абстракцій, а не від реалізацій. Сервіс-контейнер - пряме втілення:
class OrderController
{
public function __construct(private PaymentGateway $gateway) {} // інтерфейс
}
$this->app->bind(PaymentGateway::class, StripeGateway::class); // реалізація в провайдері
Користь: тестованість (легко підставити mock), гнучкість (зміна реалізації в одному місці). Водночас Senior знає, коли не переускладнювати - надмірна абстракція заради «чистоти» шкодить не менше за її відсутність.
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.
- Stateful - сервер зберігає стан клієнта між запитами (наприклад, сесія у файлі/пам'яті конкретного інстансу). Тоді потрібна «липкість» (sticky sessions) або спільне сховище.
- Stateless - сервер не зберігає стану; кожен запит самодостатній і містить усе потрібне (наприклад, JWT/токен з даними автентифікації).
Stateful: сесія на сервері → потрібен спільний Redis/sticky LB
Stateless: токен у запиті → будь-який інстанс обробить запит
Stateless легше масштабувати горизонтально - інстанси взаємозамінні. У Laravel веб-частина зазвичай stateful (сесії в Redis), API - stateless (Sanctum-токени). Для масштабування цей стан виносять у спільні Redis/БД.
Value Object - невеликий незмінний (immutable) об'єкт, що представляє концепцію домену й порівнюється за значенням, а не за ідентичністю (на відміну від Entity з id).
final class Money
{
public function __construct(
public readonly int $cents,
public readonly string $currency,
) {}
public function add(Money $other): self
{
return new self($this->cents + $other->cents, $this->currency);
}
}
Переваги: інкапсуляція правил (валюта, валідація email), самодокументований код, безпека (незмінність). У Laravel VO зручно зберігати через Custom Casts, перетворюючи між колонкою БД та об'єктом.
Одне з ключових архітектурних рішень. Контролери й моделі швидко «розпухають», тож логіку виносять в окремі класи.
Проблема «товстих» контролерів: контролер має лише приймати запит, делегувати роботу й повертати відповідь. Бізнес-логіка в ньому не тестується ізольовано й не перевикористовується.
Service-класи - групують пов'язану логіку домену:
class OrderService
{
public function __construct(
private PaymentGateway $gateway,
private InventoryManager $inventory,
) {}
public function place(User $user, Cart $cart): Order
{
return DB::transaction(function () use ($user, $cart) {
$this->inventory->reserve($cart->items);
$order = $user->orders()->create([/* ... */]);
$this->gateway->charge($user, $cart->total);
OrderPlaced::dispatch($order);
return $order;
});
}
}
Action-класи (single-action) - один клас = одна операція. Дрібніша гранулярність, дуже тестовано:
class PlaceOrderAction
{
public function handle(User $user, Cart $cart): Order { /* ... */ }
}
«Товсті» моделі - логіку, тісно пов'язану з даними самої моделі (скоупи, аксесори, прості методи стану), доречно лишати в моделі. Складні міждоменні операції - у сервіси.
Рекомендації:
- Контролер тонкий: запит → виклик сервісу/екшену → відповідь.
- Складні операції з кількома моделями - у Service/Action із транзакцією.
- Логіка одного агрегату - у моделі (скоупи, обчислювані атрибути).
- Не плодіть абстракцій передчасно - починайте простіше, виносьте за потреби.