Glossary overview

DTO

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); // чисто и понятно