Pull to refresh

DTO в языке PHP: примеры для начинающих

Level of difficultyEasy
Reading time12 min
Views28K

DISCLAIMER


Друзья, читая этот текст, вы мало того, что общаетесь со вселенским разумом, но и принимаете участие в социальном эксперименте. ChatGPT пытается рассуждать о DTO в языке PHP. Пока ему сложно, с каждым вашим комментарием, замечанием, он пытается улучшить свой ответ, получается не всегда хорошо. Мы со своей стороны его почти не редактируем. Просто просим переформулировать какие-то фрагменты, дополнить свой ответ. Скоро опубликуем статью в ВАКовском журнале об этом эксперименте. Ссылку приложим в комментариях.

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

В PHP DTO может быть реализован как класс с публичными свойствами, которые соответствуют данным, которые нужно передать. DTO может использоваться для передачи данных между слоями приложения, такими как контроллеры, сервисы, хранилища и т. д.

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

Самый простой пример реализации DTO в PHP:

class UserDTO {
    public $id;
    public $name;
    public $email;

    public function __construct($id, $name, $email) {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
    }
}

В данном примере создается DTO для пользователя, который содержит свойства «id», «name» и «email». Конструктор класса принимает значения для этих свойств и инициализирует их в объекте.

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

class UserController {
    public function getUser($userId) {
        $userData = // получение данных о пользователе из базы данных
        $user = new UserDTO($userData['id'], $userData['name'], $userData['email']);
        return $user;
    }
}

В данном примере метод «getUser» контроллера получает данные о пользователе из базы данных и создает объект DTO «UserDTO» с этими данными. Этот объект затем возвращается из метода и может использоваться другими слоями приложения.

Или еще пример. Предположим, у вас есть веб-приложение, которое содержит страницу, на которой пользователь может создавать новый профиль. Когда пользователь заполняет форму для создания профиля и отправляет ее, данные из формы должны быть переданы на сервер для обработки. В этом случае можно использовать DTO для передачи данных между слоями приложения.

class UserProfileDTO {
    public $firstName;
    public $lastName;
    public $email;
    public $password;
}

После того, как пользователь отправил форму, данные из формы могут быть использованы для создания объекта DTO «UserProfileDTO» и переданы на сервер для обработки:

// Создаем объект DTO на основе данных из формы
$userProfile = new UserProfileDTO();
$userProfile->firstName = $_POST['first_name'];
$userProfile->lastName = $_POST['last_name'];
$userProfile->email = $_POST['email'];
$userProfile->password = $_POST['password'];

// Передаем объект DTO на сервер для обработки
$userService = new UserService();
$userService->createUserProfile($userProfile);

В этом примере данные из формы используются для создания объекта DTO «UserProfileDTO», который затем передается на сервер для обработки через сервис «UserService». Объект DTO облегчает передачу данных между слоями приложения и уменьшает связанность (coupling) между ними.

DTO: иммутабельность, модификатор readonly и тайп-хинтинг


Как можно догадаться, DTO по определению является неизменяемым (immutable) объектом, то есть объектом, который не может быть изменен после создания. Это свойство делает DTO более безопасным и предсказуемым в использовании, так как исключает возможность несанкционированного изменения данных.

Для реализации неизменяемости в DTO можно использовать модификаторы «readonly» или «const» в свойствах класса. Модификатор «readonly» позволяет задать только для чтения свойство, которое может быть установлено только в момент создания объекта. Модификатор «const» позволяет задать константу, которая не может быть изменена после создания объекта.

Пример DTO с использованием модификатора «readonly»:

class UserDTO {
    public readonly int $id;
    public readonly string $name;
    public readonly string $email;
    public readonly string $phone;

    public function __construct(int $id, string $name, string $email, string $phone) {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
        $this->phone = $phone;
    }

    // Геттеры и сеттеры для свойств
    // ...
}

В данном примере определен DTO для пользователя, который содержит свойства «id», «name», «email» и «phone». Конструктор класса принимает значения для этих свойств и инициализирует их в объекте. Свойства класса определены с модификатором «readonly», что делает их доступными только для чтения и предотвращает их изменение после создания объекта.

Использование модификатора «readonly» в свойствах класса позволяет реализовать неизменяемость в DTO и сделать его более безопасным и предсказуемым в использовании.

Отдельно стоит упомянуть тайп-хинтинг свойств — это еще один способ улучшить безопасность и предсказуемость DTO. Тайп-хинтинг свойств позволяет указать тип данных для свойств класса, что обеспечивает проверку типов во время выполнения и помогает предотвратить ошибки связанные с типами данных.

Пример DTO с использованием тайп-хинтинга свойств:

class UserDTO {
    public int $id;
    public string $name;
    public string $email;
    public string $phone;

    public function __construct(int $id, string $name, string $email, string $phone) {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
        $this->phone = $phone;
    }

    // Геттеры и сеттеры для свойств
    // ...
}

В данном примере определен DTO для пользователя, который содержит публичные свойства «id», «name», «email» и «phone». Конструктор класса принимает значения для этих свойств и инициализирует их в объекте. Тайп-хинтинг свойств позволяет указать тип данных для свойств класса, что обеспечивает проверку типов во время выполнения.

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

Когда же применять DTO


Набросал небольшой список — перечень этот открытый и все случаи сложно обозреть. Обычно DTO используется в случаях, когда:
  1. Необходимо передать данные между различными слоями приложения. Например, данные, полученные из базы данных, могут быть переданы контроллеру, который затем обрабатывает эти данные и передает их сервису.
  2. Необходимо уменьшить связанность (coupling) между слоями приложения. Использование DTO позволяет каждому слою обрабатывать только необходимые данные и не зависеть от других слоев.
  3. Необходимо более явно определить данные, которые передаются между слоями. DTO позволяет определить явно, какие данные передаются между слоями приложения, что делает код более понятным и легко поддерживаемым.
  4. Необходимо обеспечить безопасность данных. Использование DTO может помочь защитить данные, так как только определенные свойства объекта DTO могут быть переданы между слоями приложения.
  5. Необходимо передать данные между различными языками программирования. Использование DTO позволяет определить явно формат данных, что упрощает их пересылку между различными языками.
  6. Необходимо передать данные по сети или между различными приложениями. Использование DTO может помочь определить формат данных, которые должны быть переданы между приложениями, что упрощает их обмен.
  7. Необходимо передать только определенные свойства объекта между слоями приложения. DTO позволяет определить явно, какие свойства объекта должны быть переданы между слоями приложения, что уменьшает объем передаваемых данных и может улучшить производительность.
  8. Необходимо сократить количество запросов к базе данных. Использование DTO может помочь уменьшить количество запросов к базе данных, так как можно передавать только необходимые данные между слоями приложения.
  9. Необходимо обработать большие объемы данных. Использование DTO может помочь упростить обработку больших объемов данных, так как можно определить явно, какие данные должны быть обработаны в каждом слое приложения.
  10. Необходимо предотвратить возможные ошибки при передаче данных между слоями. Использование DTO может помочь предотвратить возможные ошибки при передаче данных между слоями приложения, так как определение формата данных становится более явным и четким.

DTO и межъязыковое взаимодействие


Как сказано выше, DTO может быть полезным при межъязыковой передаче данных. Например, из PHP в Go, так как эти два языка имеют различные типы данных и форматы сериализации, которые могут затруднить передачу данных между ними. Использование DTO позволяет определить явно формат данных, которые должны быть переданы между PHP и Go, что упрощает их обмен.

Вот один из примеров “межъязыкового” взаимодействия с помощью DTO (пример взят из одной популярной библиотеки):

 class UserDTO {
    public $id;
    public $name;
    public $email;
    public $phone;

    public function __construct($id, $name, $email, $phone) {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
        $this->phone = $phone;
    }

    public function toJson() {
        return json_encode([
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'phone' => $this->phone,
        ]);
    }
}

В данном примере создается DTO для пользователя, который содержит свойства «id», «name», «email» и «phone». Конструктор класса принимает значения для этих свойств и инициализирует их в объекте. Также есть метод «toJson», который преобразует объект DTO в формат JSON.

Для передачи данных из PHP в Go можно использовать следующий код:

$userProfile = new UserDTO(1, 'John Doe', 'johndoe@example.com', '555-1234');
$data = $userProfile->toJson();

// Отправляем данные в Go через gRPC
$client = new UserProfileClient('localhost:50051', [
    'credentials' => Grpc\ChannelCredentials::createInsecure(),
]);
$request = new SaveUserProfileRequest();
$request->setData($data);
$response = $client->SaveUserProfile($request);

В данном примере объект DTO «UserDTO» преобразуется в формат JSON с помощью метода «toJson», затем данные передаются в Go приложение через gRPC. На стороне Go данные могут быть десериализованы из JSON и использованы в соответствующих целях.

Заметка на полях: в стандартной библиотеке PHP существует интерфейс JsonSerializable, который определяет метод jsonSerialize() для преобразования объекта в формат JSON.

Однако, для использования этого интерфейса необходимо вручную реализовывать метод jsonSerialize() для каждого объекта, что может быть неудобно и затратно по времени.

В то же время, существуют сторонние библиотеки, такие как JMS Serializer и Symfony Serializer, которые предоставляют более гибкий и удобный способ преобразования объектов в формат JSON.

Например, в библиотеке Symfony Serializer для преобразования объектов в формат JSON используется класс JsonEncoder, который автоматически преобразует объекты в формат JSON на основе их свойств и типов данных. Это позволяет сократить время и усилия, затрачиваемые на реализацию интерфейса JsonSerializable для каждого объекта.

Таким образом, использование DTO может помочь упростить передачу данных между PHP и Go, определяя явно формат данных, которые должны быть переданы между ними.

DTO и Value Object


Для реализации DTO часто используется паттерн Value Object. Однако, DTO и Value Object — это разные понятия, которые имеют разные цели и применение.

DTO не должен содержать логики и обычно имеет публичные свойства и методы доступа к ним. Value Object (VO) — это объект, который представляет некоторое значение или концепцию в приложении. VO — это неизменяемый объект, который имеет определенное значение и инкапсулирует в себе логику работы с этим значением. VO обычно не используется для передачи данных между слоями приложения, а используется для улучшения моделирования предметной области.

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

Пример VO:

class Money {
    private $amount;
    private $currency;

    public function __construct($amount, $currency) {
        $this->amount = $amount;
        $this->currency = $currency;
    }

    public function add(Money $money) {
        if ($this->currency !== $money->currency) {
            throw new Exception('Currencies do not match');
        }

        return new Money($this->amount + $money->amount, $this->currency);
    }

    public function getAmount() {
        return $this->amount;
    }

    public function getCurrency() {
        return $this->currency;
    }
}

В данном примере определен VO для представления денежных сумм, который содержит свойства «amount» и «currency». Конструктор класса принимает значения для этих свойств и инициализирует их в объекте. Также есть метод «add», который складывает две суммы и возвращает новый объект VO. Объект VO «Money» инкапсулирует логику работы с денежными суммами и может использоваться для улучшения моделирования предметной области.

Как можно предположить, Value Object (и DTO, как подкласс) не могут находиться в невалидном состоянии. Это одно из ключевых свойств паттерна и означает, что объект всегда находится в корректном состоянии.

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

Если возникает необходимость изменить значение Value Object (или DTO), то создается новый объект соответствующего класса с новым значением. Таким образом, все созданные объекты Value Object (и DTO) всегда находятся в корректном состоянии.

Валидация, как правило, выполняется на уровне создания объекта Value Object (и DTO), и если значение не соответствует ожидаемому формату, то создание объекта не производится, а генерируется исключение.

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

Валидация в конструкторе позволяет гарантировать, что объект всегда находится в корректном состоянии, что обеспечивает предсказуемость и безопасность в работе с объектом. Если значение не соответствует ожидаемому формату, то создание объекта не производится, а генерируется исключение.

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

Однако, важно помнить, что валидация не должна производиться в самом объекте Value Object (или DTO), так как это может привести к нарушению принципа единственной ответственности (Single Responsibility Principle). Задачей объекта Value Object (или DTO) является только хранение данных и предоставление доступа к этим данным. Валидация и обработка ошибок должны быть выполнены во внешнем коде.

Второй вариант (внешней) валидации более гибкий и позволяет задавать правила и сценарии валидации с помощью метаданных.

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

Например, вместо того, чтобы проверять каждое свойство объекта Value Object (или DTO) в конструкторе, можно использовать метаданные для определения правил и сценариев валидации для каждого свойства. Таким образом, внешний код может легко задавать правила и сценарии валидации без необходимости изменения кода объекта Value Object (или DTO).

Кроме того, использование метаданных для валидации позволяет легко расширять правила и сценарии валидации при изменении требований к приложению.

Например, вместо жестко закодированных правил валидации в конструкторе объекта Value Object (или DTO), можно использовать внешние файлы или базы данных для хранения метаданных правил валидации. Таким образом, можно быстро и легко изменять правила и сценарии валидации без необходимости изменения кода объекта Value Object (или DTO).

Аттрибуты и сценарии валидации


С помощью атрибутов (attributes) в PHP можно задавать метаданные, в том числе правила и сценарии валидации.

Пример использования атрибутов для задания правил валидации с помощью фреймворка Symfony:

use Symfony\Component\Validator\Constraints as Assert;

class UserDTO {
    #[Assert\NotBlank]
    #[Assert\Email]
    public string $email;

    #[Assert\NotBlank]
    public string $password;

    public function __construct(string $email, string $password) {
        $this->email = $email;
        $this->password = $password;
    }
}

В данном примере определен DTO для пользователя, который содержит свойства «email» и «password». Для задания правил валидации используются атрибуты из компонента Symfony\Validator\Constraints.

Например, атрибут #[Assert\NotBlank] задает правило валидации, что значение свойства не должно быть пустым. Атрибут #[Assert\Email] задает правило валидации, что значение свойства должно быть корректным email адресом.

Value Object и Entity


В наше повествование следует ввести также одно из ключевых понятий ООП — Entity. И VO и Entity представляют некоторый объект в приложении, но имеют разные цели и применение.

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

Entity — это объект, который представляет сущность в приложении. Сущность — это объект, который имеет свое состояние и идентификатор, и может быть отслеживаем системой хранения данных. Entity может быть изменен в процессе работы приложения и обычно имеет связи с другими сущностями в приложении.

Разница между VO и Entity заключается в их целях и применении. VO используется для представления некоторого значения или концепции в приложении и инкапсулирует в себе логику работы с этим значением, в то время как Entity используется для представления сущности в приложении, имеет свое состояние и идентификатор, и может быть отслеживаем системой хранения данных.

Пример Entity:

class User {
    private $id;
    private $name;
    private $email;

    public function __construct($id, $name, $email) {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
    }

    public function getId() {
        return $this->id;
    }

    public function getName() {
        return $this->name;
    }

    public function getEmail() {
        return $this->email;
    }

    public function setName($name) {
        $this->name = $name;
    }

    public function setEmail($email) {
        $this->email = $email;
    }
}

В данном примере определена Entity для представления пользователя, который содержит свойства «id», «name» и «email». Конструктор класса принимает значения для этих свойств и инициализирует их в объекте. Также есть методы геттеров и сеттеров для свойств.

Entity «User» представляет сущность пользователя в приложении и может быть отслеживаем системой хранения данных. Entity имеет свое состояние и идентификатор (поле «id»), и может быть изменен в процессе работы приложения.
Tags:
Hubs:
Total votes 14: ↑5 and ↓9-4
Comments36

Articles