В чем разница и когда что использовать? Это был один из вопросов, на которые я пытался получить ответ.
Попытаюсь тут описать ту практику, которую считаю не плохой. С примерами на PHP
. Постараюсь описывать на простом языке - без использования сложной терминологии.
Лучше всего это два подхода понимаются в слоистой архитектуре(слой презентации
, доменный
,
, инфраструктураприложение
- те, которые на текущий момент я использую в итоговом примере).
DTO(Data Transfer Object)
DTO - это объект класса необходим для передачи структурированной информации из одного места(метода, функции, слоя) в другое.
Важный момент передавать стараться только скаляры(не объекты), может быть исключения, но лучше попытаться все же не использовать объекты.
Передача данных, где много параметров на примере отправки письма:
// у нас есть функция отправки заказного письма - внутри который мы эмитируем выбор города
// Передача через параметры
// тут мы вызываем ее и передаем набор данных
// Проблемы: много параметров,
function sendMail1(
string $name,
string $family,
string $country,
string $city,
string $street,
int $numberHome,
?int $room = null
) {
selectCity($city);
}
sendMail1('Иван', 'Иванов', 'Россия', 'Ставрополь', 'ул.Мира', 2, 186);
// Передача через ассоциативный массив
// Проблемы:
// нужно точно знать название ключа (подсказок совсем нет)
// проверять на существование ключа (может быть sity или вообще не быть)
// проверять соответствие типу данных (придет null где не надо или строка вместо числа)
// может содержать неконтролируемый поток информации(например при передаче request()) и без дебаггера вообще не разобраться что находится внутри
function sendMail2(array $client)
{
selectCity($client['city']);
}
$client = [
'name' => 'Иван',
'family' => 'Иванов',
'country' => 'Россия',
'city' => 'Ставрополь',
'street' => 'ул.Мира',
'numberHome' => 2,
'room' => 186
];
sendMail2($client);
// Использование DTO
// Должен быть максимально простой и без возможности изменения (readonly)
function sendMail3(ClientMailDTO $client)
{
selectCity($client->city);
}
final readonly class ClientMailDTO
{
public function __construct(
public string $name,
public string $family,
public string $country,
public string $city,
public string $street,
public int $numberHome,
public ?int $room = null
) {}
}
sendMail3(new ClientMailDTO(
'Иван',
'Иванов',
'Россия',
'Ставрополь',
'ул.Мира',
2,
186
));
Из плюсов DTO
- это больше удобство, которое обычно выражается при использовании "слоистых" приложений или сервисов, чтоб выглядел более лаконично.
Что же касается практической пользы то это:
Не изменяемость значений, после их передачи
-
более удобный способ обращений к коллекции принимаемых данных, так как IDE подсвечивает возможные варианты. Особенно удобно, когда много входящих данных и есть подобно названные внутренние переменные.
Подсказка phpstorm
-
Если нам необходимо получить из какого то метода эти данные, то тут - однозначно только DTO (причину по которой не подходит массив - описано выше)
// Возвращаем данные для отправки письма в виде DTO function getMailData(): ClientMailDTO { ... return new ClientMailDTO( 'Иван', 'Иванов', // и т.д. ) }
Минусы - увеличивает объем кода и усложняет проект(опять же, зависит от контекста).
php < 8
Если у Вас нет readonly
код будет немного длиннее, но в целом тоже можно использовать
class ClientMailPhp7DTO
{
public function __construct(
private string $name,
private string $family,
private string $country,
private string $city,
private string $street,
private int $numberHome,
private ?int $room = null
) {}
public function getName(): string
{
return $this->name;
}
public function getFamily(): string
{
return $this->family;
}
// ... дальше по аналогии
}
Value object (объект значение)
VO - задача передать уже не просто данные, а так сказать валидированные на сколько это возможно.
В основном нужен для слоистой архитектуры, а именно доменного слоя или бизнес логики.
class NumberHomeVO{
public function __construct(
readonly public int $value
) {
// Номер дома должен быть больше 0
if ($this->value<0){
throw new Exception('Не корректный номер дома');
}
}
}
function repairHome(NumberHomeVO $home)
{
// Оправляем рабочего на этот номер дома))
SendWorker($home->value);// тут мы точно знаем, что значение корректное(на сколько это возможно для дальнейшей обработки(поиска по бд или создании в бд))
}
VO Так же могут быть и составными, когда корректность данных зависит ни от одного элемента, а от нескольких
class ManyVO
{
/**
* @throws Exception
*/
public function __construct(
readonly public int $summa,
readonly public string $currency
) {
if ($this->currency !== 'USD' || $this->currency !== 'RUB') {
throw new Exception('Не корректная сумма денег');
}
}
}
То есть на выходе мы получаем корректный объект данных, со стороны бизнес требований(определенная валюта)
Проверки могут быть сложнее и их количество может быть гораздо больше
Пакет для упрощения проверки на валидность
Для проверок можно так же использовать пакет `Webmozart\Assert;`
use Webmozart\Assert\Assert;
/**
* Проверка email на валидность
*/
final readonly class Email
{
public function __construct(
public string $value,
) {
Assert::notEmpty($this->value);
Assert::email($this->value);
}
}
Реализация VO
Используем объекты реализованные на основе классов VO для передачи в конструктор, таким образом внутри класса Post
мы получаем валидные данные, с которыми уже можем работать - не переживая о некорректном содержании.
final class Post
{
public function __construct(
public TitleVO $name,
public EmailVO $emailVO,
//... и т.д.
) {}
}
Теперь работа с DTO и VO на примере
Пример старался написать максимально простым, что хотелось показать:
DTO - для передачи данных из слоя
PRESENTATION
в слойAPPLICATION
VO - для валидации и преобразования данных в какие то бизнес сущности
-
Обработка идет так же по всем слоям, то есть уровень понимания
исключения
на каждом слое свой, как и обработка его. НапримерПрезентация - что то не так
Приложение - записать в лог ошибку, что б дальше дебажить
// PRESENTATION
// передаем в сервис только простые данные из приходящих откуда-то(например по API)
//Controller
class PostController
{
public function create(Request $request)
{
try {
PostService::create(new PostCreateDTO(
$request->get('title'),
$request->get('text'),
));
// ответ 201 - все хорошо
} catch (Exception $e) {
// ответ 400 - не корректный запрос
}
}
}
// APPLICATION
// В приложении описываем - с какими данными будем работать
class PostCreateDTO
{
public function __construct(
readonly public string $title,
readonly public string $text,
) {}
}
class PostService
{
/**
* @throws Exception
*/
static function create(PostCreateDTO $DTO): void
{
// преобразуем простые данные в уже логически корректные данные и создаем (бизнес корректную, валидную) Post сущность
try {
$post = new PostModel(
new TitleVO($DTO->title),
new ContentVO($DTO->text)
);
} catch (Exception $e) {
// тут выбираем действия, если создать не вышло
Log::error($e->getMessage());
throw new Exception('Проверьте корректность данных');
}
// ... как то дальше сохраняем пост или же что-то еще делаем
}
}
// DOMAIN
// тут описываем какие у нас будут правила по этому объекту
// у VO выкидываем исключения, что б дальше обработать их
class TitleVO
{
/**
* @throws Exception
*/
public function __construct(
readonly public string $value,
) {
if (strlen($this->value) < 2) {
throw new Exception('Не корректное название');
}
}
}
class ContentVO
{
/**
* @throws Exception
*/
public function __construct(
readonly public string $value,
) {
if (empty($this->value) || strlen($this->value) < 40) {
throw new Exception('Не корректное содержимое');
}
}
}
final class PostModel
{
public function __construct(
TitleVO $title,
ContentVO $content,
) {}
}
Код написал больше для понимания использования DTO и VO, максимально упрощенный. Так же не писал про инфраструктуру
, счет ее излишней в примере.
Что же касается SOLID принципов, DDD, чистой архитектуры и т.д. старался не освещать в текущей статье, что бы порог вхождения(понимание) было как можно более комфортным.
Комментарии (9)
TsarS
08.06.2025 08:52Интересней в примере получение данный в DTO - например из сервиса/queryBus в DTO
FanatPHP
08.06.2025 08:52Спасибо за статью. Но не удержусь от пары замечаний.
Ошибку валидации вряд ли нужно логировать. Это, по сути, не ошибка, а совершенно ожидаемое поведение. Да, там останется только перевыброс исключения. И как раз на этом стоит остановиться поподробнее
Код не станет сложнее, если Exception поменять на ServiceValidationException и DomainValidationException, но зато станет куда более логичным и приближённым к реальности. А сейчас смотришь, и не понимаешь, почему ошибка, к примеру, соединения базы данных возвращает 400, а не 500 статус
И вот тут как раз и объяснить, почему вы меняем DomainValidationException на ServiceValidationException
Ну и совсем уж по мелочи
ошибку "Не корректная сумма денег" надо заменить на "Некорректная валюта" (и в целом надо бы вычитать на грамматические ошибки и опечатки)
проверка на empty($this->value) не имеет смысла, её стоит убрать
Ivan6463 Автор
08.06.2025 08:52Насчет замечания по поводу исключений - такой подход так же имеет место быть. Я же хотел - как можно максимальнее сосредоточить статью на DTO и VO. Что бы все внимание досталось именно им.
Что же касается - логирования - в текущем контексте - это был лишь инструмент для того, что б показать абстрактный пример обработки исключения на одном из слое. Возможно нужно было - подобрать немного иную абстракцию.
'empty' поправил(убрал) - спасибо, Вы правы)
Rsa97
08.06.2025 08:52Проблемы: много параметров,
Это не такая уж и проблема, особенно учитывая то, что для создания DTO вам нужно использовать точно такое же количество параметров. При этом, параметры функции
SendMail1
точно так же отображаются в подсказках IDE, как и подсказки функции__construct
классаClientMailDTO
.if ($this->currency !== 'USD' || $this->currency !== 'RUB') {
А здесь есть другое решение -
Enum
с нужными валютами. При этом IDE будет подсказывать, какие варианты допустимы в качестве параметра функции.
vanxant
08.06.2025 08:52ValueObject - неудачное название. Под этим часто понимают класс с единственным полем value (часто даже публичным), смысл которого - запретить сомнительные операции. Например, id часто представляет собой целое число, но это число - вещь в себе. Ни умножать ни складывать его ни с чем нельзя, но при этом можно сравнивать (т.е. вычитать с отбрасыванием результата). Вот здесь и нужен тип IntValue, который при этом умеет сериализовываться / десериализовываться в число.
Ivan6463 Автор
08.06.2025 08:52Насчет название и смысла - так может показаться, если прийти к
ValueObject
через интуицию(то есть не прибегая к специализированному материалу).
Например логика может быть такая: переводится как объект значение - ну тут все понятно)) Пойду кодить)))
Что же касается смысла, причин использования и примеры на мой взгляд интересных реализаций - это можно подсмотреть в книге Вон Верон "Предметно-ориентированное проектирование". Там достаточно подробно это все описано.
Опять же это мое видение.
А что касается конкретного названия - смею предположить, что нужно было как то назвать этот подход и тут"Объект-значение"
в контексте архитектурногоподхода
и в контекстесинтаксиса
выбранного языка программирование - может означатьразный смысл
.
В первом случае(контекст подхода
) - этоосмысление
текущего объекта со стороныбизнес логики
Во втором случае(контекст синтаксиса ЯП
) - можно предположить, как раз таки, что у объекта естькакое то значение
и ни про какую бизнес логику или осмысление тут уже речь не идет.
И закончу ответ фразой: "Отвергаешь - предлагай". То есть, какое название с Вашей точки зрения - было бы удачное?vanxant
08.06.2025 08:52То есть, какое название с Вашей точки зрения - было бы удачное?
Ну, эээ, собственно DTO. Ну если мы про PHP, то в симфони например создать невалидную дтошку из запроса можно, с целью например потом подтянуть какие-то значения ещё откуда-то, но вот пробросить по шине и обработать невалидную дто уже нельзя, шина не пропустит.
Ну и надо различать валидацию (поле email содержит что-то, что выглядит как емейл) и верификацию (например, бизнес-логика требует, что этот емейл должен быть в БД, иначе сначала иди подтверди его через отправку письма с кодом).
fo_otman
Это разве не чистая архитектура? Контроллер - Use Case - Сервис - Сущность.
Ivan6463 Автор
У DDD подобный подход, и у луковой архитектуры тоже. То есть вышеперечисленные используют подобный общий подход. Думаю Ваше суждение верное, как и если бы сказали, что это DDD - то тоже было бы верно