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

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

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

19 питань

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

Стандартна структура (app/Models, app/Http/Controllers) групує код за типом. На сотнях класів зміна однієї фічі зачіпає десять тек, а залежності між частинами не видно.

Групування за предметною областю (модулі):

app/
  Billing/      Models, Actions, Events, Policies, BillingServiceProvider
  Catalog/
  Shipping/
  Shared/       спільне: value objects, базові класи

Laravel не нав'язує структуру - простори імен PSR-4 і провайдери дозволяють так робити без пакетів.

Що робить модулі справжніми, а не лише теками:

  • Публічна поверхня. Інші модулі звертаються до Billing через його дії чи сервіси, а не до таблиць і моделей напряму.
  • Події між модулями. OrderPlaced з Catalog слухає Billing - відправник не знає про отримувача.
  • Свої провайдери реєструють маршрути, політики, слухачі модуля.
  • Перевірка меж архітектурними тестами: arch()->expect('App\Billing')->not->toUse('App\Shipping\Models').

Чого уникати:

  • переносити структуру DDD повністю на CRUD-застосунок - шари без логіки лише додають файлів;
  • спільних «God»-моделей (User з методами всіх модулів) - кожен модуль може мати свій погляд на користувача;
  • ділити на мікросервіси замість модулів: межі спершу перевіряють усередині моноліту, де помилку дешево виправити.

Докладніше в документації: Структура каталогів

Модель природно притягує все, що стосується сутності: запити, обчислення, правила, побічні дії. Розподіл за видами відповідальності:

Лишається в моделі - те, що описує саму сутність: зв'язки, касти, $fillable, короткі методи стану (isPaid()).

Виноситься:

  • запити - у скопи (#[Scope] методи) або власний Query Builder моделі, коли скопів багато;
  • перетворення значень - у касти й об'єкти-значення (Money, Address) замість пари аксесорів на кожне поле;
  • логіка над набором моделей - у власну колекцію (#[CollectedBy]);
  • бізнес-операції («оформити замовлення», «повернути кошти») - у дії чи сервіси, а не методи Order::refund() на 80 рядків з HTTP-викликами;
  • побічні дії - у події й слухачі, явні в коді, а не приховані в booted();
  • правила доступу - у політики;
  • повторювана поведінка кількох моделей (slug, мультитенантність, архівування) - у трейти з boot{Trait}().

Ознаки, що час ділити: модель імпортує сервіси й HTTP-клієнти, методи потребують моків у тестах, частина методів використовується лише в одному місці.

Мета не «тонка модель будь-якою ціною», а щоб кожна зміна мала одне очевидне місце.

Докладніше в документації: Локальні скопи