100 советов: модульный монолит на Laravel + DDD

100 советов для разработчика модульного монолита на Laravel с DDD. Универсальные практики — не привязаны к конкретному проекту.

10 разделов × 10 рекомендаций. Каждый раздел завершается примером кода.

* * *

  • 1. Один модуль = один bounded context. Группируйте по бизнес-способности (Billing, Catalog), а не по техническим слоям.
  • 2. Модуль должен быть удаляемым из проекта с минимальным числом правок в соседних модулях.
  • 3. Не создавайте модуль Shared для всего подряд — только kernel для действительно общего.
  • 4. Именуйте модули по домену: User, Order, Payment — не Common, Utils, Helpers.
  • 5. Каждый модуль регистрирует свой ServiceProvider и свои маршруты.
  • 6. Миграции модуля живут внутри модуля, не в корневой database/migrations.
  • 7. Запретите прямые импорты Infrastructure соседнего модуля — только через порты.
  • 8. Документируйте публичный API модуля: Contracts и Ports.
  • 9. Начинайте с 3–5 модулей, дробите только при реальной боли.
  • 10. Модульный монолит — самостоятельная архитектура, а не обязательный этап к микросервисам.

modules/
├── Catalog/
├── Order/
├── Payment/
└── Shared/  ← избегайте
* * *

  • 11. Domain не зависит от Laravel, Eloquent, HTTP, очередей.
  • 12. Сущности — rich model: методы changeEmail(), cancel(), а не только геттеры.
  • 13. Приватный конструктор + create() и restore() для сущностей.
  • 14. Value Object для ID, Email, Money, Slug — валидация в одном месте.
  • 15. Инварианты проверяйте внутри сущности, не в контроллере.
  • 16. Domain Events — только при реальной нужде, не ради паттерна.
  • 17. Write-репозиторий (интерфейс) в Domain, реализация в Infrastructure.
  • 18. Domain Service — для логики, не принадлежащей одной сущности.
  • 19. Доменные исключения — свои классы с понятными сообщениями.
  • 20. Не импортируйте Request, Response, Collection из Laravel в Domain.

final readonly class Email
{
    private function __construct(private string $value) {}

    public static function fromString(string $value): self
    {
        if (! filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidEmail($value);
        }
        return new self(strtolower($value));
    }
}
* * *

  • 21. Один use case = одна папка: CreateOrder/Command + Handler.
  • 22. Command и Query — отдельные классы, не один Service на всё.
  • 23. Handler — final readonly, один публичный метод handle().
  • 24. Handler оркестрирует, не содержит SQL и HTTP.
  • 25. Не передавайте Request в Handler — только Command/Query DTO.
  • 26. Транзакции — на уровне Handler или Repository, не в Action.
  • 27. Application не возвращает Eloquent Model — Entity или DTO.
  • 28. Валидируйте формат на границе, бизнес-правила — в Domain.
  • 29. Handler не знает, откуда вызов: API, CLI, MCP, очередь.
  • 30. Логирование — через декоратор или middleware, не в каждом Handler.

final readonly class CreateOrderHandler
{
    public function handle(CreateOrderCommand $cmd): OrderId
    {
        $order = Order::create(/* ... */);
        $this->repository->save($order);
        return $order->id();
    }
}
* * *

  • 31. Eloquent Model — только в Infrastructure, никогда в Domain.
  • 32. Mapper обязателен: toEntity() и toModel().
  • 33. Repository реализует интерфейс Domain, скрывает детали БД.
  • 34. Не используйте Eloquent relationships между моделями разных модулей.
  • 35. Read-репозитории делают join и возвращают DTO, не Entity.
  • 36. ACL-адаптеры реализуют чужие Ports в Infrastructure/ACL/{Module}/.
  • 37. Кэш — Infrastructure concern, инвалидируйте из Handler.
  • 38. Job вызывает Handler, не дублирует бизнес-логику.
  • 39. Миграции — в папке модуля.
  • 40. Seeders модуля — только для своих таблиц.

final class OrderMapper
{
    public function toEntity(OrderModel $m): Order { /* restore */ }
    public function toModel(Order $e, OrderModel $m): OrderModel { /* ... */ }
}
* * *

  • 41. Action — тонкий: DTO → Command → Handler → Response.
  • 42. Invokable Action лучше контроллера с десятком методов.
  • 43. FormRequest или Spatie Data — валидация на входе.
  • 44. Action не вызывает Eloquent напрямую.
  • 45. Один Action = один endpoint = один use case.
  • 46. Data-объекты для ответов, не сырые массивы.
  • 47. MCP, Console, HTTP — разные адаптеры, один Handler.
  • 48. Middleware для auth, не проверки прав в каждом Action.
  • 49. Ошибки домена → HTTP-коды в Exception Handler.
  • 50. Версионирование API — на уровне маршрутов.

final readonly class CreateOrderAction
{
    public function __invoke(CreateOrderRequest $req): JsonResponse
    {
        $id = $this->handler->handle($req->toCommand());
        return response()->json(['id' => $id->value()], 201);
    }
}
* * *

  • 51. Разделяйте write (Command) и read (Query) по папкам.
  • 52. CommandBus не обязателен — DI и явный Handler достаточно.
  • 53. Read-модель — плоские DTO под экран.
  • 54. Write-модель — Entity и доменные правила.
  • 55. Отдельные интерфейсы: OrderRepository и OrderReadRepository.
  • 56. Одна БД для CQRS-lite — разные запросы, не разные базы.
  • 57. Списки — Query, изменения — Command.
  • 58. Пагинация и фильтры — в Read Repository.
  • 59. Event sourcing — только при доказанной необходимости.
  • 60. При росте нагрузки — read из view или материализованных таблиц.

Application/Command/CreateOrder/   ← write
Application/Query/ListOrders/      ← read
Domain/Repository/OrderRepository
* * *

  • 61. Consumer owns Port — потребитель объявляет интерфейс.
  • 62. Provider реализует в ACL соседнего модуля.
  • 63. Contracts — публичный read-only API модуля.
  • 64. Не импортируйте Entity чужого модуля без веской причины.
  • 65. Синхронные вызовы — через порты, не HTTP внутри монолита.
  • 66. Domain Events — осторожно, без over-engineering.
  • 67. Shared Kernel — минимум: общие VO, базовые исключения.
  • 68. ACL (Anticorruption Layer) защищает от чужих моделей.
  • 69. Документируйте экспортируемые Contracts.
  • 70. Циклические зависимости — сигнал пересмотреть границы.

Order/Ports/PaymentGateway.php
  ← Payment/Infrastructure/ACL/Order/PaymentGatewayAdapter.php
* * *

  • 71. Unit-тесты Domain — без БД, на инварианты и VO.
  • 72. Unit-тесты Handler — мокайте Repository и Ports.
  • 73. Feature-тесты API — полный путь через HTTP.
  • 74. Не тестируйте Eloquent в Domain-тестах.
  • 75. Фабрики данных — в модуле, не глобальные God-factories.
  • 76. Architecture tests (PHPArkitect, Deptrac) — в CI.
  • 77. Тесты модуля — в Modules/X/Tests/.
  • 78. Contract tests при нескольких реализациях порта.
  • 79. Не гонитесь за 100% coverage — покрывайте критичное.
  • 80. Regression test на каждый баг в доменной логике.

public function test_order_cannot_be_cancelled_twice(): void
{
    $order = Order::restore(/* cancelled */);
    $this->expectException(OrderAlreadyCancelled::class);
    $order->cancel();
}
* * *

  • 81. PHPArkitect или Deptrac — автопроверка слоёв.
  • 82. PHPStan/Larastan level 8+ для типизации.
  • 83. Pint — единый стиль кода.
  • 84. Baseline для архитектурных правил — исправляйте постепенно.
  • 85. pre-commit: тесты + статический анализ.
  • 86. Запрет merge при падении architecture tests.
  • 87. Scaffold модуля — artisan-команда с правильной структурой.
  • 88. ADR для ключевых архитектурных решений.
  • 89. Code review: слой, зависимости, тесты.
  • 90. Рефакторинг маленькими PR.

composer phparkitect  # в CI pipeline
* * *

  • 91. God Service на 2000 строк — разбейте на Handlers.
  • 92. Eloquent в Controller — всегда плохо.
  • 93. Модуль Common/Utils со всем подряд — хаос.
  • 94. Active Record как Domain Entity — анемичная модель.
  • 95. CQRS с первого дня без нагрузки — преждевременная оптимизация.
  • 96. Микросервисы «на вырост» — начните с монолита.
  • 97. DTO везде без причины — лишняя сложность.
  • 98. Event Sourcing для простого CRUD — overkill.
  • 99. Копипаста между модулями вместо Port/Contract.
  • 100. Архитектура ради архитектуры — код должен помогать бизнесу.

// ❌ Order::create($request->all()) в контроллере
// ✅ $handler->handle($request->toCommand()) в Action
* * *

Модульный монолит с DDD — дисциплина границ и зависимостей. Начните с чётких модулей, чистого Domain и тонкого Presentation.

Сборник для code36.ru

Комментарии

Загрузка…

Войдите, чтобы оставить комментарий