Executando verificação de segurança...
8

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.

Carregando publicação patrocinada...
3
2

Artigo muito bom!!!
Precisamos usar mais esse tipo de conceito! Acabamos pensando, não maioria das vezes, em modo procedural ao invés de pensar no modo orientado a objetos.