DTO (Data Transfer Object)
DTO (Data Transfer Object - Objeto de Transferência de Dados) é um padrão de projeto que funciona como um contêiner para transportar dados entre diferentes camadas de uma aplicação. Pense nele como uma "mala organizada" que carrega informações de um lugar para outro, mantendo tudo estruturado e seguro.
Imagine que você está construindo uma loja online. Quando um usuário cria uma conta, o formulário do frontend envia os dados para sua API. Aqui está onde o DTO entra:
DTO é simplesmente uma "caixa organizada" que transporta dados de um lugar para outro na sua aplicação.
Por Que DTOs Existem?
Em aplicações web modernas, você tem várias camadas (layers): Controller (recebe requisições), Service (lógica de negócio), Repository (banco de dados). Passar dados entre essas camadas usando múltiplos parâmetros soltos ou expondo diretamente suas entidades de banco de dados cria problemas graves de manutenção e segurança.
Problema Real: Imagine um método que recebe 10 parâmetros. Agora adicione mais um campo. Você precisa alterar a assinatura do método em todos os lugares onde ele é chamado. Com DTO, você adiciona uma propriedade e pronto.
Anatomia de um DTO
DTO de Entrada (Input)
Recebe dados vindos do cliente (frontend, app mobile, outra API):
class CriarUsuarioDTO
{
public function __construct(
public readonly string $nome,
public readonly string $email,
public readonly string $senha,
public readonly ?string $telefone // opcional
) {}
// Factory method para criar a partir de Request
public static function fromRequest(Request $request): self
{
return new self(
nome: $request->input('nome'),
email: $request->input('email'),
senha: $request->input('senha'),
telefone: $request->input('telefone')
);
}
}
DTO de Saída (Output)
Retorna dados para o cliente, ocultando informações sensíveis:
class UsuarioResponseDTO
{
public function __construct(
public readonly int $id,
public readonly string $nome,
public readonly string $email,
public readonly string $criadoEm
// Note: senha NÃO está aqui!
) {}
// Factory method para criar a partir de Model
public static function fromModel(User $user): self
{
return new self(
id: $user->id,
nome: $user->nome,
email: $user->email,
criadoEm: $user->created_at->format('d/m/Y H:i')
);
}
}
Por que separar Input e Output? Dados que você recebe são diferentes dos que você retorna. No input você precisa de senha para criar usuário, mas no output você NUNCA deve retornar senha, mesmo que hasheada.
Fluxo Completo em Aplicação Web
Frontend Envia Requisição
// React/Vue enviando POST
fetch('/api/produtos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
nome: "Notebook Dell",
preco: 3500.00,
estoque: 10,
categoriaId: 5
})
})
Controller Recebe e Processa
class ProdutoController
{
public function __construct(
private ProdutoService $service
) {}
public function criar(Request $request)
{
// 1. Validação dos dados recebidos
$validated = $request->validate([
'nome' => 'required|min:3|max:255',
'preco' => 'required|numeric|min:0.01',
'estoque' => 'required|integer|min:0',
'categoriaId' => 'required|exists:categorias,id'
]);
// 2. Converte dados validados em DTO
$dto = new CriarProdutoDTO(
nome: $validated['nome'],
preco: $validated['preco'],
estoque: $validated['estoque'],
categoriaId: $validated['categoriaId']
);
// 3. Passa DTO para Service
$produto = $this->service->criar($dto);
// 4. Converte Model em DTO de resposta
$response = ProdutoResponseDTO::fromModel($produto);
// 5. Retorna JSON
return response()->json($response, 201);
}
}
Service Executa Lógica de Negócio
class ProdutoService
{
public function criar(CriarProdutoDTO $dto): Produto
{
// Lógica de negócio complexa
// Verifica se já existe produto com mesmo nome
if (Produto::where('nome', $dto->nome)->exists()) {
throw new ValidationException('Produto já existe');
}
// Aplica desconto se categoria específica
$precoFinal = $dto->preco;
if ($dto->categoriaId === 5) {
$precoFinal *= 0.9; // 10% desconto
}
// Cria produto
$produto = Produto::create([
'nome' => $dto->nome,
'preco' => $precoFinal,
'estoque' => $dto->estoque,
'categoria_id' => $dto->categoriaId
]);
// Dispara evento
event(new ProdutoCriado($produto));
return $produto;
}
}
Resposta Retorna ao Cliente
{
"id": 42,
"nome": "Notebook Dell",
"preco": 3150.00,
"estoque": 10,
"categoria": "Eletrônicos",
"criadoEm": "25/12/2025 23:53"
}
Boas Práticas Essenciais
Mantenha DTOs Simples e Planos
// ✅ BOM: DTO simples
class EnderecoDTO
{
public function __construct(
public readonly string $rua,
public readonly string $numero,
public readonly string $cidade,
public readonly string $estado,
public readonly string $cep
) {}
}
// ❌ RUIM: DTO com lógica de negócio
class EnderecoDTO
{
// ...
public function calcularFrete(): float
{
// NÃO! Lógica de negócio não vai aqui
return $this->estado === 'SP' ? 10.00 : 25.00;
}
}
Use DTOs Separados por Caso de Uso
// DTO para criar
class CriarUsuarioDTO
{
public function __construct(
public readonly string $nome,
public readonly string $email,
public readonly string $senha
) {}
}
// DTO para atualizar (pode ter campos diferentes)
class AtualizarUsuarioDTO
{
public function __construct(
public readonly ?string $nome,
public readonly ?string $telefone
// Note: sem senha e email aqui
) {}
}
// DTO para resposta
class UsuarioResponseDTO
{
public function __construct(
public readonly int $id,
public readonly string $nome,
public readonly string $email,
public readonly bool $ativo
) {}
}
Torne DTOs Imutáveis
Use readonly (PHP 8.1+) para garantir que dados não sejam modificados após criação:
class ProdutoDTO
{
public function __construct(
public readonly string $nome, // não pode ser alterado
public readonly float $preco
) {}
}
Nomeação Clara e Específica
// ✅ BOM: Nomes descritivos
CriarPedidoDTO
AtualizarEnderecoDTO
ListarProdutosResponseDTO
// ❌ RUIM: Nomes genéricos
PedidoDTO // Criar? Atualizar? Resposta?
DadosDTO // Dados de quê?
Validação no DTO ou Fora?
Recomendação: Validação básica (tipos, estrutura) pode estar no DTO via type hints. Validação complexa (regras de negócio) deve ficar no Controller ou Service:
// Validação no Controller
$validated = $request->validate([
'email' => 'required|email|unique:users',
'idade' => 'required|integer|min:18'
]);
// DTO recebe dados já validados
$dto = CriarUsuarioDTO::fromArray($validated);
Exemplo Completo: Sistema de E-commerce
DTOs Necessários
// Input: Criar pedido
class CriarPedidoDTO
{
public function __construct(
public readonly int $usuarioId,
public readonly array $itens,
public readonly int $enderecoId,
public readonly string $formaPagamento,
public readonly ?string $cupomDesconto
) {}
}
// Input: Item do pedido
class ItemPedidoDTO
{
public function __construct(
public readonly int $produtoId,
public readonly int $quantidade
) {}
}
// Output: Pedido criado
class PedidoResponseDTO
{
public function __construct(
public readonly int $id,
public readonly string $numero,
public readonly float $valorTotal,
public readonly string $status,
public readonly array $itens,
public readonly string $criadoEm
) {}
}
Controller
public function finalizarCompra(Request $request)
{
$validated = $request->validate([
'itens' => 'required|array|min:1',
'itens.*.produtoId' => 'required|exists:produtos,id',
'itens.*.quantidade' => 'required|integer|min:1',
'enderecoId' => 'required|exists:enderecos,id',
'formaPagamento' => 'required|in:cartao,boleto,pix',
'cupomDesconto' => 'nullable|exists:cupons,codigo'
]);
$dto = new CriarPedidoDTO(
usuarioId: auth()->id(),
itens: array_map(
fn($item) => new ItemPedidoDTO($item['produtoId'], $item['quantidade']),
$validated['itens']
),
enderecoId: $validated['enderecoId'],
formaPagamento: $validated['formaPagamento'],
cupomDesconto: $validated['cupomDesconto'] ?? null
);
$pedido = $this->pedidoService->criar($dto);
return response()->json(
PedidoResponseDTO::fromModel($pedido),
201
);
}
Quando NÃO Usar DTOs
DTOs não são necessários em toda situação:
- Métodos simples:
buscarPorId(int $id)não precisa de DTO - Operações CRUD básicas: Se você só faz operações simples sem transformação
- Projetos muito pequenos: O overhead pode não valer a pena
- Comunicação interna: Entre métodos da mesma classe
Benefícios Reais em Projetos
Segurança
// Sem DTO: expõe tudo
return response()->json($user); // inclui senha_hash, tokens, etc
// Com DTO: controle total
return response()->json(UsuarioResponseDTO::fromModel($user)); // só dados seguros
Manutenibilidade
Adicionar campo novo? Muda apenas o DTO. Todos os lugares que usam o DTO continuam funcionando com type safety (segurança de tipos).
A Essência do DTO
DTOs são contêineres de dados que transitam entre camadas da aplicação. Eles mantêm seu código organizado, seguro e fácil de manter ao separar claramente o que entra, o que é processado, e o que sai da sua aplicação. Use-os quando precisar desacoplar camadas, proteger dados sensíveis, ou facilitar manutenção futura do seu projeto web.