Skip to content

Архитектура & паттерны — лучшие практики

Дополняет ../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/BladeGod Object, не тестируетсяService

Внутренняя база знаний студии