Тема
Архитектура & паттерны — лучшие практики
Дополняет
../code-quality.md,../services.md,../controllers.md.
Слои: Controller → Service/Action → Model
HTTP Request
→ FormRequest (валидация)
→ Controller (тонкий: принял → вызвал → вернул, ≤10 строк)
→ Service/Action (бизнес-логика, транзакции, побочные эффекты)
→ Model (данные, связи, casts)Контроллер НЕ знает про БД напрямую, Service НЕ знает про Request/Response.
Service vs Action
| Подход | Когда | Форма |
|---|---|---|
| Service | домен с несколькими операциями | OrderService с методами place(), cancel(), refund() |
| Action (single-purpose) | одно действие, переиспользуется | PlaceOrderAction::execute() / __invoke() |
Оба валидны. Action — когда операция атомарна и тестируется изолированно. Service — когда методы делят зависимости/состояние домена. Не смешивать «God Service на 1000 строк» (нарушает SRP — дроби по доменам).
php
final readonly class PlaceOrderAction
{
public function __construct(
private InventoryService $inventory,
private PaymentGateway $payments,
) {}
public function execute(PlaceOrderData $data): Order
{
return DB::transaction(function () use ($data) {
$order = Order::create($data->toArray());
$this->inventory->reserve($order);
$this->payments->charge($order, $data->paymentToken);
return $order;
});
}
}DTO вместо ассоциативных массивов
Передача данных между слоями — типизированный readonly-объект, не array:
php
final readonly class PlaceOrderData
{
public function __construct(
public int $userId,
public array $items,
public string $paymentToken,
public ?string $promoCode = null,
) {}
public static function fromRequest(PlaceOrderRequest $r): self
{
return new self(
userId: $r->user()->id,
items: $r->validated('items'),
paymentToken: $r->validated('payment_token'),
promoCode: $r->validated('promo_code'),
);
}
}Преимущество: IDE-автокомплит, типобезопасность, не «магические ключи массива». Пакет spatie/laravel-data автоматизирует (валидация + DTO + каст из request).
Dependency Injection
- Constructor injection — всегда. НЕ
app()/resolve()внутри методов (скрытая зависимость). - Интерфейс → реализация:
bindвAppServiceProvider::register(). singleton— только когда состояние живёт весь request lifecycle.- Property promotion (PHP 8) +
readonlyдля зависимостей.
php
public function __construct(
private readonly OrderService $orders,
private readonly LoggerInterface $logger,
) {}Полиморфизм вместо if по типу (OCP)
php
// Плохо: switch разрастается с каждым типом
match ($type) { 'boost' => ..., 'paid' => ..., 'free' => ... };
// Хорошо: Enum с поведением
enum Tier: string {
case Boost = 'boost';
case Paid = 'paid';
case Free = 'free';
public function priorityWeight(): int {
return match ($this) {
self::Boost => 100, self::Paid => 50, self::Free => 0,
};
}
}Явность > магия
| Избегать | Почему | Вместо |
|---|---|---|
$with в модели | грузит всегда, незаметно | ->with() явно |
| Global scopes (кроме SoftDelete/Tenancy) | меняют поведение скрыто | явный scope-вызов |
| Observer с бизнес-логикой | трудно дебажить, скрытые эффекты | Event + Listener с явным Service |
| Макросы без причины | магия, непонятно откуда метод | документированный pain-point в ADR |
Error handling
php
// Плохо — глотает ошибку
try { ... } catch (\Exception $e) { return null; }
// Хорошо — domain exception + единая обработка в bootstrap/app.php
throw_unless($order->canCancel(), OrderCannotBeCancelledException::class);- Domain exceptions:
OrganizationNotFoundException extends DomainException. throw_if()/throw_unless()вместо вложенныхif.- Рендеринг ошибок (API/HTML) — централизованно в
->withExceptions(). - Логировать или пробрасывать — никогда не глотать молча.
Типизация
declare(strict_types=1)во всех PHP-файлах бэкенда.- Типы параметров и return — всегда. Generics в PHPDoc:
@return Collection<int, Order>. - Larastan (PHPStan) level 5+ в CI — ловит type-ошибки статически.
Anti-patterns
| Паттерн | Проблема | Решение |
|---|---|---|
| Fat controller (>10 строк логики) | смешение слоёв | Service/Action |
| God Service (1000+ строк) | нарушает SRP | дробить по доменам |
app(X::class) в методе | скрытая зависимость | constructor DI |
| Массив вместо DTO | нет типов, магические ключи | readonly DTO |
catch (\Exception) { return null } | потеря ошибок | domain exception / log |
| Бизнес-логика в Model/Controller/Middleware/Blade | God Object, не тестируется | Service |