Когда мы говорим о Domain-Driven Design, один из частых вопросов, которые я слышу:
“А нужно ли вообще делать валидацию в Entity? Это же бизнес-логика, а не проверка форм.”
Ответ на самом деле — да и нет. Зависит от сути валидации.
Разберёмся по порядку.
✅ Что действительно должно быть в Entity
Если коротко: всё, что относится к бизнес-инвариантам, должно валидироваться на уровне Entity.
Инварианты — это такие правила, которые всегда должны быть соблюдены, независимо от того, как ты создаёшь объект:
через API, из CLI, через Event, или даже в тесте.
Примеры инвариантов:
| Правило | Почему в Entity? |
|---|---|
| Имя пользователя не может быть пустым | Это требование бизнес-домена |
| Возраст пользователя ≥ 14 лет | Это логика продукта |
| Email не должен повторяться (если это важно на уровне домена) | Это правило модели |
Если ты не проконтролируешь это в Entity — твой домен становится хрупким. Создать невалидного пользователя сможет любой, у кого есть доступ к new User(...).
🚫 А что лучше оставить за пределами Entity?
Не стоит превращать Entity в форму. Есть проверки, которые не имеют отношения к бизнесу — они касаются специфики взаимодействия с внешним миром:
| Проверка | Где её делать? |
|---|---|
Номер телефона в формате +7... | В DTO или Symfony Validator |
| Файл должен быть JPEG ≤ 5 МБ | В инфраструктуре (например, в хендлере загрузки) |
Поле description обязательно | В форме или DTO |
| Капча или токен подтверждения | На уровне Application |
Entity — не полицейский, проверяющий каждое поле. Он — гарант доменной целостности.
🔍 Почему нельзя полагаться только на DTO
DTO — это всего лишь транспорт, оболочка вокруг входящих данных.
Сегодня ты получил их из API, завтра из теста, послезавтра из Kafka.
Если проверка “имя не может быть пустым” живёт только в DTO, то:
$user = new User('');
спокойно создаст “битого” пользователя где-нибудь в другом месте кода. И ты даже не узнаешь об этом — пока не станет слишком поздно.
🧪 Хорошая практика
Простой и рабочий способ:
final class User
{
public function __construct(private string $name)
{
if (mb_strlen($name) < 2) {
throw new \DomainException('Имя слишком короткое');
}
}
}
Так ты гарантируешь, что User всегда создаётся в валидном состоянии.
И тебе уже не страшны ошибки из-за пропущенной валидации в 148 месте кода.
⚖️ Компромисс: фабрики и named constructors
Если ты не хочешь “мусорить” Entity исключениями, можешь обернуть валидацию в фабрику:
final class UserFactory
{
public static function fromDto(UserCreateDTO $dto): User
{
if (mb_strlen($dto->name) < 2) {
throw new \DomainException('Имя слишком короткое');
}
return new User($dto->name);
}
}
Но всё равно — валидация, относящаяся к бизнесу, должна быть рядом с моделью.
🧩 Как это сформулировать одной фразой?
Entity отвечает за соблюдение бизнес-инвариантов. DTO — за валидацию пользовательского ввода.
📌 Вывод
Если ты работаешь по DDD, помни:
твоё ядро — это место силы. Оно должно быть самодостаточным.
Иначе ты просто сделал ещё одну анемичную модель с кучей getter’ов и null-сущностей.
Если статья была полезна — буду рад обратной связи. А если ты видишь ситуацию по-другому — пиши, обсудим как взрослые люди в комментариях.