← Назад к статьям
Архитектура PHP и паттерны проектирования

Что такое DTO в PHP и зачем оно нужно?

Узнайте, что такое Data Transfer Object (DTO) в PHP, зачем он нужен для типобезопасности и чистой архитектуры, и как реализовать его с современными возможностями PHP 8.

Доступные языки
Что такое DTO в PHP и зачем оно нужно?
Рекомендуемая статья ↗

Что такое DTO в PHP и зачем оно нужно?

При создании PHP-приложений разработчики постоянно сталкиваются с задачей передачи данных между различными слоями приложения: контроллерами, сервисами, репозиториями и представлениями. Распространённый антипаттерн — передача сырых массивов или объектов stdClass, что приводит к неясным контрактам, скрытым багам и сложному рефакторингу. Именно здесь на помощь приходят Data Transfer Objects (DTO).

Что такое DTO?

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

Термин популяризировал Мартин Фаулер в своей книге Patterns of Enterprise Application Architecture. В мире PHP DTO получили широкое распространение с ростом Domain-Driven Design (DDD) и чистой архитектуры.

Зачем нужны DTO?

Использование DTO вместо массивов или обобщённых объектов даёт несколько важных преимуществ:

  • Типобезопасность — у DTO строго типизированные свойства, что позволяет рано выявлять ошибки и улучшает автодополнение в IDE.
  • Ясные контракты — DTO явно определяет, какие данные он несёт, в отличие от обычного массива с неизвестными ключами.
  • Иммутабельность — DTO можно сделать только для чтения, что предотвращает случайное изменение данных при их перемещении по системе.
  • Инкапсуляция — структура данных отделена от бизнес-логики, что делает код чистым и сфокусированным.
  • Простой рефакторинг — изменение структуры DTO — это точечное изменение, а не поиск по десяткам обращений к массивам.
  • Самодокументируемость — сам класс описывает форму данных.

Простой пример DTO в PHP

Создадим базовый DTO для пользователя:

<?php declare(strict_types=1); final class UserDTO { public function __construct( public readonly int $id, public readonly string $name, public readonly string $email, public readonly int $age, ) { } }

Благодаря readonly-свойствам и продвижению свойств в конструкторе (PHP 8.1+) этот DTO является лаконичным, иммутабельным и типобезопасным.

Использование DTO на практике

Вот как можно использовать DTO при возврате данных из репозитория:

<?php final class UserRepository { public function findById(int $id): ?UserDTO { $row = $this->db->fetchOne('SELECT * FROM users WHERE id = ?', [$id]); if ($row === null) { return null; } return new UserDTO( id: (int) $row['id'], name: $row['name'], email: $row['email'], age: (int) $row['age'], ); } }

Теперь вызывающий код точно знает, какие данные он получает:

<?php $user = $repository->findById(42); if ($user !== null) { echo $user->name; // IDE знает, что это строка echo $user->email; // IDE знает, что это строка }

DTO vs. Value Object vs. Entity

Важно не путать DTO с другими паттернами:

  • DTO — переносит данные, без поведения, без идентичности.
  • Value Object — иммутабельный, определяется своими значениями, может содержать поведение (например, Money, Email).
  • Entity — имеет уникальную идентичность и жизненный цикл, содержит бизнес-логику.

Преобразование массивов в DTO

В реальных приложениях часто нужно построить DTO из массива, например, из тела запроса. Распространённый подход — статический фабричный метод:

<?php final class CreateUserDTO { public function __construct( public readonly string $name, public readonly string $email, public readonly int $age, ) { } public static function fromArray(array $data): self { return new self( name: $data['name'], email: $data['email'], age: (int) $data['age'], ); } }

DTO и валидация

DTO не должны выполнять валидацию самостоятельно. Вместо этого держите валидацию в отдельном слое, например, в form request или сервисе-валидаторе. DTO создаётся только после того, как данные прошли валидацию.

<?php $data = $request->getParsedBody(); $validator->validate($data, [ 'name' => 'required|string', 'email' => 'required|email', 'age' => 'required|integer|min:18', ]); $dto = CreateUserDTO::fromArray($data);

Когда DTO могут быть избыточны

DTO нужны не всегда. В небольших скриптах или прототипах использование массивов может быть вполне уместным. Введение DTO имеет смысл, когда:

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

Лучшие практики для DTO в PHP

  • Делайте их иммутабельными — используйте readonly-свойства или приватные свойства с геттерами.
  • Держите их маленькими — DTO должен представлять одну ясную концепцию.
  • Никакой бизнес-логики — переносите любую логику в сервисы или доменные объекты.
  • Используйте строгие типы — объявляйте strict_types=1 в каждом файле DTO.
  • Предоставляйте фабричные методы, такие как fromArray() или fromRequest(), для удобного создания.
  • Избегайте сеттеров — DTO должен создаваться один раз и никогда не изменяться.

Заключение

DTO — это простой, но мощный паттерн, который приносит ясность, типобезопасность и поддерживаемость в PHP-приложения. Они определяют явные контракты между слоями, уменьшают количество багов, вызванных слабо типизированными массивами, и значительно упрощают рефакторинг. Хотя они добавляют немного шаблонного кода, долгосрочные выгоды в средних и крупных проектах намного перевешивают затраты. Начните с малого: внедрите DTO там, где данные пересекают границы слоёв, и вы быстро почувствуете разницу в качестве кода.

Попробуйте отрефакторить один из ваших потоков данных на основе массивов в DTO уже сегодня — ваше будущее «я» скажет вам спасибо.

Технологии и темы

Теги статьи

Нет статей, соответствующих этим фильтрам.

Есть проект или идея для обсуждения?

Давайте обсудим ↗