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
Комментарии
Войдите, чтобы оставить комментарий