Найти в Дзене

Паттерн DTO (Data Transfer Object)

Data Transfer Object или же DTO или же в переводе на русский язык - "объект передачи данных" - это шаблон проектирования приложения, который позволяет упаковывать все необходимые данные в один экземпляр класса и передавать их в любой участок вашего приложения в удобном виде. Все начинается с проблемы. Допустим мы создаем кусок приложения, который создает кусок пользователя. Для начала мы создаем метод createUser , который будет выглядеть примерно так: public function createUser(string $email, string $password): User
{
return User::create([
'email' => $email,
'password' => bcrypt($password)
]);
} Не вдаваясь в подробности откуда взялся класс User и что за функция bcrypt можно увидеть, что мы передаем 2 аргумента: email и пароль пользователя. Затем создаем пользователя. Следом появляется задача добавить контактные данные пользователя. К примеру: и прочие данные. Что делать в такой ситуации? Передавать десяток аргументов? точно нет, так как в этих аргументах можно будет потом заблу
Оглавление

Data Transfer Object или же DTO или же в переводе на русский язык - "объект передачи данных" - это шаблон проектирования приложения, который позволяет упаковывать все необходимые данные в один экземпляр класса и передавать их в любой участок вашего приложения в удобном виде.

Проблема

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

public function createUser(string $email, string $password): User
{
return User::create([
'email' => $email,
'password' => bcrypt($password)
]);
}

Не вдаваясь в подробности откуда взялся класс User и что за функция bcrypt можно увидеть, что мы передаем 2 аргумента: email и пароль пользователя. Затем создаем пользователя.

Следом появляется задача добавить контактные данные пользователя. К примеру:

  • фамилия
  • имя
  • отчество
  • дата рождения

и прочие данные. Что делать в такой ситуации? Передавать десяток аргументов? точно нет, так как в этих аргументах можно будет потом заблудиться, их будет тяжело поддерживать, они не все обязательные и у части придется проставлять просто null. Второе что приходит на ум это передача массива. Это конечно имеет место быть если забыть про такие аспекты как отказоустойчивость приложения и читаемость кода.

При передаче массива мы должны точно знать какие элементы мы ожидаем. Если какие-то не обязательные, то обработать их отсутствие и описать в PHPDoc или аннотациях что будет входить в передаваемый массив.

Тогда у нас получится следующий код:

/**
* @param array{
* email: string,
* password: string,
* name: ?string,
* surname: ?string,
* patronymic: ?string,
* is_guest: ?bool
* } $data
*/
public function createUser(array $data): User
{
return User::create([
'email' => $data['email'],
'password' => bcrypt($data['password']),
'name' => $data['name'],
'surname' => $data['surname'],
'patronymic' => $data['patronymic'] ?? null,
'is_guest' => $data['is_guest'] ?? true,
]);
}

В целом система рабочая, но сразу видны минусы:

  • Большой кусок PHPDoc
  • Что будет если массив придет пустой?
  • Что будет если какое-то из обязательных полей не отправится
  • Нужно обрабатывать каждое значение по умолчанию отдельно
  • Этот массив надо где-то сформировать прежде чем отправить

и так далее

Что предлагает паттер DTO

Вместо того, чтобы плодить множество аргументов метода или же формировать и обрабатывать массивы, можно создать класс. Назвать его, к примеру, CreateUserDTO и описать всю структуру там.

Вот пример:
class CreateUserDTO
{
public function __construct(
public string $email,
public string $password,
public string $name,
public string $surname,
public ?string $patronymic,
public bool $isGuest = true,
)
{

}
}

Затем собрать его в нужном месте

$dto = new CreateUserDTO(
email: 'test@ya.ru',
password: bcrypt('password'),
name: 'Иван',
surname: 'Иванов'
);

И передать и обработать в самом методе

public function createUser(CreateUserDTO $dto): User
{
return User::create([
'email' => $dto->email,
'password' => $dto->password,
'name' => $dto->name,
'surname' => $dto->surname,
'patronymic' => $dto->patronymic,
'is_guest' => $dto->isGuest,
]);
}

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

Подробнее про класс с DTO

Если посмотреть на класс CreateUserDTO, описанный выше, то можно заметить, что он и нарушает ООП принципы, так как имеет все публичные свойства, доступные из вне, так и нарушает правила чистого кода Роберта Мартина о том, что метод не должен принимать больше 3х аргументов, но этому есть объяснение.

К такому классу стоит относится больше как к структуре данных, а не как к классу. То есть мы просто сформировали конкретную структуру с конкретными правилами заполнения и дали возможность ее передавать и считывать. Но стоит относится к этому без фанатизма

О фанатизме

В примере выше представлен идеальный вариант DTO. только публичные поля в конструкторе без каких либо методов. Еще раз повторюсь, что DTO это лишь структура данных. Многие могут подумать "а что если я сделаю метод toArray, который обернет эту структуру в массив" или же "добавлю я валидацию переданного email чтобы избежать кривых данных", но нет. Так делать нельзя. DTO - это лишь структура и в ней не должно быть никакой логики. Только данные. Иначе это будет похоже уже на сервис, entity или же value object, но не на DTO