Value object и DTO в PHP (DDD)

от автора

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

Попытаюсь тут описать ту практику, которую считаю не плохой. С примерами на PHP. Постараюсь описывать на простом языке — без использования сложной терминологии.

Лучше всего это два подхода понимаются в слоистой архитектуре(слой презентации, доменный, инфраструктура, приложение — те, которые на текущий момент я использую в итоговом примере).

DTO(Data Transfer Object)

DTO — это объект класса необходим для передачи структурированной информации из одного места(метода, функции, слоя) в другое.

Важный момент передавать стараться только скаляры(не объекты), может быть исключения, но лучше попытаться все же не использовать объекты.

  1. Передача данных, где много параметров на примере отправки письма:

// у нас есть функция отправки заказного письма - внутри который мы эмитируем выбор города  // Передача через параметры // тут мы вызываем ее и передаем набор данных // Проблемы: много параметров, 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 — это больше удобство, которое обычно выражается при использовании «слоистых» приложений или сервисов, чтоб выглядел более лаконично.

Что же касается практической пользы то это:

  1. Не изменяемость значений, после их передачи

  2. более удобный способ обращений к коллекции принимаемых данных, так как IDE подсвечивает возможные варианты. Особенно удобно, когда много входящих данных и есть подобно названные внутренние переменные.

    Подсказка phpstorm

    Подсказка phpstorm
  3. Если нам необходимо получить из какого то метода эти данные, то тут — однозначно только 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 на примере

Пример старался написать максимально простым, что хотелось показать:

  1. DTO — для передачи данных из слоя PRESENTATION в слой APPLICATION

  2. VO — для валидации и преобразования данных в какие то бизнес сущности

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

    1. Презентация — что то не так

    2. Приложение — записать в лог ошибку, что б дальше дебажить

// 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 (empty($this->value) || 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, чистой архитектуры и т.д. старался не освещать в текущей статье, что бы порог вхождения(понимание) было как можно более комфортным.


ссылка на оригинал статьи https://habr.com/ru/articles/916590/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *