Как стать автором
Обновить

Value object и DTO в PHP (DDD)

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров2.6K

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

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

Теги:
Хабы:
+6
Комментарии9

Публикации

Работа

PHP программист
74 вакансии

Ближайшие события