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

Питання на співбесіді: Архітектура

Найпопулярніші питання з реальних 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-застосунку спільнота вважає це антипатерном.

Шлях запиту:

  1. public/index.php - єдина точка входу; підключає автозавантажувач Composer.
  2. Створюється екземпляр застосунку (Service Container) із bootstrap/app.php.
  3. HTTP Kernel обробляє запит, завантажує Service Providers (registerboot).
  4. Запит проходить глобальні middleware (наприклад, обробка сесій, CSRF).
  5. Router зіставляє URL із маршрутом, виконуються middleware маршруту.
  6. Викликається контролер/замикання, формується Response.
  7. Відповідь проходить 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 із транзакцією.
  • Логіка одного агрегату - у моделі (скоупи, обчислювані атрибути).
  • Не плодіть абстракцій передчасно - починайте простіше, виносьте за потреби.

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