DTO — это Data Transfer Object (объект для передачи данных), известный шаблон проектирования.

Проблема

Откуда взялся DTO, зачем он вообще нужен и можно ли без него?

Сразу отвечу. Без него — можно. Но с ним иногда удобнее.

Представим себе обычный метод. Какой-нибудь метод для создания заказа.

public function createOrder()

Что нам минимально потребуется для заказа? Как-то идентифицировать пользователя, а также указать что именно он купил и сколько заплатил.

public function createOrder(string $email, string $goods, int $sum)

Наш бизнес пошёл успешно и у нас добавляются новые параметры:

  1. Промокод
  2. Скидка
  3. Имя менеджера оформившего покупку, для расчёта бонусов от продаж
  4. Телефон покупателя
  5. Канал продаж, для отслеживания рекламных кампаний по UTM-меткам

Пожалуй, хватит.

Объявление метода раздулось:

public function createOrder(
		string $email, 
		string $goods, 
		int $sum,
		string $promocode,
		int $discount,
		string $manager,
		string $phone,
		string $utmChannel,
	)

Выглядит устрашающе. Но это не предел. Параметров может стать гораздо больше, и все они необходимы нам для полноценного описания заказа.

Теперь в каждом месте в коде где используется этот метод, мы получим “лесенку” из параметров. Если продукт большой, то заказ может быть создан не только в одном месте, а во многих. Он может быть создан из консоли, по API, из разных контроллеров с разных пользовательских форм на сайте. В тестах.

При каждом добавлении, изменении, удалении параметра в этом методе мы вынуждены тщательно обновлять все места, где он упоминается. Но с большим количеством параметров очень легко запутаться и допустить ошибку.

Как сократить количество параметров?

Решение

Мы возьмём все параметры и сгруппируем их в один очень простой объект.

class OrderDto
{
	public string $email;
	public string $goods;
	public int $sum;
	public string $promocode;
	public int $discount;
	public string $manager;
	public string $phone;
	public string $utmChannel;
}

Теперь мы можем передавать в метод этот объект, указав в методе лишь его тип, а не перечисляя все параметры.

public function createOrder(OrderDto $order)

Метод сократился и код стал более читаемым.

Параметры не исчезли и “переехали” в создание объекта, но теперь они явно обозначены:

$order = new OrderDto();
$order->email = 'fox@mulder.com';
$order->goods = 'Alien Blaster';
$order->sum = 5000;
...

Каждому параметру соответствует имя и уже не нужно подглядывать в определение метода, чтобы понять за что отвечает N-й по счёту параметр.

Почему не использовать модель ORM?

Действительно, в модели ORM как правило перечислены все те же самые поля.

Почему бы просто не создавать такую модель и не передавать её везде вместо того чтобы делать отдельный класс DTO?

Резонный вопрос.

Дело в том, что в этом случае знания о деталях общения с БД проникают во весь остальной код, который с БД напрямую не работает.

Из-за этого, понадобится нам переименовать колонку в таблице — и мы вынуждены править модель везде, включая контроллеры.

Будет затронуто больше кода при изменениях, а следовательно увеличивается вероятность сделать ошибку.

Программисты которые много раз обжигались на том, что код приложения очень тесно связан со структурой БД, стараются этого не допускать и отделять БД от остального кода, что как раз и можно реализовать с помощью DTO.

Что не является DTO?

По определению, DTO это “просто данные”, то есть голая структура данных.

  1. Модель ORM не может являться DTO так как содержит как минимум связь с БД, а иногда и логику валидации и фильтрации и прочую.

  2. Часто путают шаблоны проектирования DTO и Value Object.

Некоторые разработчики даже утвеждают, что разницы вообще нет и Value Object является DTO.

Но DTO в отличие от Value Object не должен содержать логику. Именно отсутствие логики и делает DTO таким удобным универсальным средством для передачи любых структурированных данных.

Нам достаточно взглянуть на список полей, чтобы понять что содержится в DTO, конечно, если поля названы как следует.

Пока мы соблюдаем эту простоту — поля без логики — наш объект будет полноценным DTO.

Плюсы

Код с DTO становится более строгим и чистым, сокращается количество параметров в методах.

Но это не всё.

Также мы можем использовать DTO:

  1. Для расчёта скидки в отдельном сервисе

    $this->fillDiscount($order); // Посчитали скидку, записали в поле "discount"
    ...
    $this->createOrder($order);
    
  2. Для того чтобы вынести всю фильтрацию и валидацию запроса в отдельное место и “получить” чистый объект

    $order = $this->readOrder($request); // Все поля заполнили из запроса
    ...
    $this->createOrder($order);
    
  3. Для того чтобы описать структуры внешнего API: того API к которому подключается наше приложение