Питання на співбесіді: Архітектура
Питання з реальних співбесід з відповідями: 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 із транзакцією.
- Логіка одного агрегату - у моделі (скопи, обчислювані атрибути).
- Не плодіть абстракцій передчасно - починайте простіше, виносьте за потреби.
Стандартна структура (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-клієнти, методи потребують моків у тестах, частина методів використовується лише в одному місці.
Мета не «тонка модель будь-якою ціною», а щоб кожна зміна мала одне очевидне місце.