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 используется в случаях, когда:
- Необходимо передать данные между различными слоями приложения. Например, данные, полученные из базы данных, могут быть переданы контроллеру, который затем обрабатывает эти данные и передает их сервису.
- Необходимо уменьшить связанность (coupling) между слоями приложения. Использование DTO позволяет каждому слою обрабатывать только необходимые данные и не зависеть от других слоев.
- Необходимо более явно определить данные, которые передаются между слоями. DTO позволяет определить явно, какие данные передаются между слоями приложения, что делает код более понятным и легко поддерживаемым.
- Необходимо обеспечить безопасность данных. Использование DTO может помочь защитить данные, так как только определенные свойства объекта DTO могут быть переданы между слоями приложения.
- Необходимо передать данные между различными языками программирования. Использование DTO позволяет определить явно формат данных, что упрощает их пересылку между различными языками.
- Необходимо передать данные по сети или между различными приложениями. Использование DTO может помочь определить формат данных, которые должны быть переданы между приложениями, что упрощает их обмен.
- Необходимо передать только определенные свойства объекта между слоями приложения. DTO позволяет определить явно, какие свойства объекта должны быть переданы между слоями приложения, что уменьшает объем передаваемых данных и может улучшить производительность.
- Необходимо сократить количество запросов к базе данных. Использование DTO может помочь уменьшить количество запросов к базе данных, так как можно передавать только необходимые данные между слоями приложения.
- Необходимо обработать большие объемы данных. Использование DTO может помочь упростить обработку больших объемов данных, так как можно определить явно, какие данные должны быть обработаны в каждом слое приложения.
- Необходимо предотвратить возможные ошибки при передаче данных между слоями. Использование 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»), и может быть изменен в процессе работы приложения.