DTO (Data Transfer Object)
DTO — это простой объект который переносит данные между слоями приложения. Никакой бизнес-логики — только данные.
Проблема без DTO
// Передаём сырой массив между слоями
class UserController
{
public function store(Request $request): void
{
$data = $request->all(); // массив — нет типизации, нет гарантий
$this->userService->create($data); // что внутри? непонятно
}
}
class UserService
{
public function create(array $data): void
{
// Надеемся что в массиве есть нужные ключи
$name = $data['name']; // а вдруг нет?
$email = $data['email']; // а вдруг опечатка в ключе?
}
}
Массив не даёт никаких гарантий — IDE не подсказывает, PHP не проверяет.
Решение с DTO
// Простой DTO — только данные
class CreateUserDTO
{
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly string $password,
public readonly string $role = 'user',
) {}
}
class UserController
{
public function store(array $request): void
{
// Создаём DTO из запроса
$dto = new CreateUserDTO(
name: $request['name'],
email: $request['email'],
password: $request['password'],
);
$this->userService->create($dto);
}
}
class UserService
{
public function create(CreateUserDTO $dto): void
{
// IDE подсказывает, PHP проверяет типы
echo "Создаём {$dto->name} с email {$dto->email}" . PHP_EOL;
}
}
readonly — важная деталь
readonly означает что поле можно установить только один раз — в конструкторе. После этого изменить нельзя:
$dto = new CreateUserDTO('John', '[email protected]', '123456');
$dto->name = 'Jane'; // Error: Cannot modify readonly property
DTO должен быть неизменяемым — создал, передал, использовал.
Фабричный метод в DTO
Удобно добавить статический метод для создания из массива:
class CreateUserDTO
{
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly string $password,
public readonly string $role = 'user',
) {}
public static function fromArray(array $data): self
{
return new self(
name: $data['name'],
email: $data['email'],
password: $data['password'],
role: $data['role'] ?? 'user',
);
}
public static function fromRequest(array $request): self
{
return new self(
name: $request['name'],
email: $request['email'],
password: $request['password'],
);
}
}
// Использование
$dto = CreateUserDTO::fromArray($data);
$dto = CreateUserDTO::fromRequest($request);
Реальный пример — несколько DTO
// DTO для создания
class CreateOrderDTO
{
public function __construct(
public readonly int $userId,
public readonly int $productId,
public readonly int $quantity,
public readonly string $address,
) {}
}
// DTO для обновления
class UpdateOrderDTO
{
public function __construct(
public readonly int $orderId,
public readonly ?string $status = null,
public readonly ?string $address = null,
) {}
}
// DTO для ответа — то что возвращаем клиенту
class OrderResponseDTO
{
public function __construct(
public readonly int $id,
public readonly string $status,
public readonly int $amount,
public readonly string $createdAt,
) {}
public function toArray(): array
{
return [
'id' => $this->id,
'status' => $this->status,
'amount' => $this->amount,
'created_at' => $this->createdAt,
];
}
}
class OrderService
{
public function create(CreateOrderDTO $dto): OrderResponseDTO
{
echo "Создаём заказ для юзера {$dto->userId}" . PHP_EOL;
// Возвращаем DTO ответа — не сырую модель
return new OrderResponseDTO(
id: 42,
status: 'new',
amount: 1000,
createdAt: '2026-02-28',
);
}
public function update(UpdateOrderDTO $dto): void
{
echo "Обновляем заказ #{$dto->orderId}" . PHP_EOL;
if ($dto->status !== null) {
echo "Новый статус: {$dto->status}" . PHP_EOL;
}
if ($dto->address !== null) {
echo "Новый адрес: {$dto->address}" . PHP_EOL;
}
}
}
// Использование
$service = new OrderService();
$createDto = new CreateOrderDTO(
userId: 1,
productId: 42,
quantity: 2,
address: 'Kyiv, Ukraine',
);
$response = $service->create($createDto);
print_r($response->toArray());
$updateDto = new UpdateOrderDTO(
orderId: 42,
status: 'paid',
);
$service->update($updateDto);
DTO vs Array vs Model
// Массив — нет типизации, IDE не помогает
$data = ['name' => 'John', 'email' => '[email protected]'];
// Модель — содержит бизнес-логику, привязана к БД
$user = User::find(1);
$userService->create($user); // зачем тащить всю модель?
// DTO — только данные, типизировано, IDE подсказывает
$dto = new CreateUserDTO('John', '[email protected]', '123456');
$userService->create($dto); // чисто и понятно