Value Objects e DTOs
1. Introdução aos Conceitos
1.1 Definição de Value Object (VO)
Value Objects são objetos imutáveis cuja identidade é definida pelos valores que carregam, não por um identificador único. Diferentemente de entidades (como Usuario com ID), dois VOs são considerados iguais se todos os seus atributos forem equivalentes. Eles encapsulam validação e comportamento relacionado ao valor que representam.
Características essenciais:
- Imutabilidade: uma vez criados, seus valores não podem ser alterados
- Identidade por valor: dois VOs com mesmos atributos são equivalentes
- Auto-validação: o construtor garante que apenas estados válidos existam
1.2 Definição de Data Transfer Object (DTO)
DTOs são objetos simples projetados para transportar dados entre camadas da aplicação, especialmente através de limites de processo (APIs, camada de apresentação). Eles não contêm lógica de negócio — apenas propriedades públicas ou getters/setters.
1.3 Diferenças Fundamentais
| Característica | Value Object | DTO |
|---|---|---|
| Imutabilidade | Sim (obrigatória) | Não (opcional) |
| Comportamento | Pode ter métodos de domínio | Apenas transporte |
| Validação | Interna (no construtor) | Externa (antes da criação) |
| Identidade | Por valor | Por referência |
| Uso típico | Camada de domínio | Camada de aplicação/API |
2. Implementando Value Objects em PHP
2.1 Classe Imutável Base
<?php
declare(strict_types=1);
class Email
{
private string $valor;
public function __construct(string $valor)
{
if (!filter_var($valor, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException("Email inválido: $valor");
}
$this->valor = $valor;
}
public function valor(): string
{
return $this->valor;
}
public function equals(Email $outro): bool
{
return $this->valor === $outro->valor;
}
public static function aPartirDe(string $valor): self
{
return new self($valor);
}
}
2.2 Value Objects com Validação Complexa
<?php
class CPF
{
private string $numero;
public function __construct(string $numero)
{
$numero = preg_replace('/\D/', '', $numero);
if (strlen($numero) !== 11 || !$this->validarDigitos($numero)) {
throw new \InvalidArgumentException("CPF inválido: $numero");
}
$this->numero = $numero;
}
public function formatado(): string
{
return preg_replace('/(\d{3})(\d{3})(\d{3})(\d{2})/', '$1.$2.$3-$4', $this->numero);
}
public function equals(CPF $outro): bool
{
return $this->numero === $outro->numero;
}
private function validarDigitos(string $cpf): bool
{
// Lógica de validação dos dígitos verificadores
for ($t = 9; $t < 11; $t++) {
$d = 0;
for ($c = 0; $c < $t; $c++) {
$d += $cpf[$c] * (($t + 1) - $c);
}
$d = ((10 * $d) % 11) % 10;
if ($cpf[$c] != $d) return false;
}
return true;
}
}
class Moeda
{
private int $centavos;
public function __construct(int $centavos)
{
if ($centavos < 0) {
throw new \InvalidArgumentException("Valor não pode ser negativo");
}
$this->centavos = $centavos;
}
public function centavos(): int
{
return $this->centavos;
}
public function formatado(): string
{
return 'R$ ' . number_format($this->centavos / 100, 2, ',', '.');
}
public function soma(Moeda $outra): self
{
return new self($this->centavos + $outra->centavos);
}
}
3. Tipos Avançados de Value Objects
3.1 Value Object Composto
<?php
class Endereco
{
public function __construct(
private string $logradouro,
private string $cidade,
private string $cep
) {
if (empty($logradouro) || empty($cidade)) {
throw new \InvalidArgumentException("Logradouro e cidade são obrigatórios");
}
if (!preg_match('/^\d{5}-?\d{3}$/', $cep)) {
throw new \InvalidArgumentException("CEP inválido");
}
}
public function equals(Endereco $outro): bool
{
return $this->logradouro === $outro->logradouro
&& $this->cidade === $outro->cidade
&& $this->cep === $outro->cep;
}
}
3.2 CollectionVO Imutável
<?php
class ItensPedidoVO
{
/** @var array<ItemPedidoVO> */
private array $itens;
public function __construct(ItemPedidoVO ...$itens)
{
$this->itens = $itens;
}
public function adicionar(ItemPedidoVO $item): self
{
$novosItens = $this->itens;
$novosItens[] = $item;
return new self(...$novosItens);
}
public function total(): Moeda
{
$total = new Moeda(0);
foreach ($this->itens as $item) {
$total = $total->soma($item->subtotal());
}
return $total;
}
}
3.3 Integração com PHP 8.1+ Enums
<?php
enum StatusPedido: string
{
case PENDENTE = 'pendente';
case CONFIRMADO = 'confirmado';
case ENVIADO = 'enviado';
case ENTREGUE = 'entregue';
case CANCELADO = 'cancelado';
public function permiteCancelamento(): bool
{
return match($this) {
self::PENDENTE, self::CONFIRMADO => true,
default => false
};
}
}
4. Implementando DTOs em PHP
4.1 DTO Simples com PHP 8.1+
<?php
class CriarUsuarioDTO
{
public function __construct(
public readonly string $nome,
public readonly string $email,
public readonly string $cpf,
public readonly ?string $telefone = null
) {}
}
4.2 DTO com Factory Method
<?php
class CriarPedidoDTO
{
public function __construct(
public readonly string $clienteId,
public readonly array $itens,
public readonly ?string $observacao = null
) {}
public static function createFromArray(array $dados): self
{
return new self(
clienteId: $dados['cliente_id'],
itens: $dados['itens'],
observacao: $dados['observacao'] ?? null
);
}
}
class ItemPedidoDTO
{
public function __construct(
public readonly string $produtoId,
public readonly int $quantidade,
public readonly int $precoCentavos
) {}
}
4.3 DTOs Aninhados
<?php
class PedidoResponseDTO
{
/** @param ItemResponseDTO[] $itens */
public function __construct(
public readonly string $id,
public readonly string $cliente,
public readonly array $itens,
public readonly string $total,
public readonly string $status
) {}
}
class ItemResponseDTO
{
public function __construct(
public readonly string $produto,
public readonly int $quantidade,
public readonly string $subtotal
) {}
}
5. Mapeamento entre DTOs e Value Objects
5.1 Convertendo DTO em Value Objects
<?php
class PedidoMapper
{
public static function paraDominio(CriarPedidoDTO $dto): array
{
$itens = [];
foreach ($dto->itens as $itemDTO) {
$itens[] = new ItemPedidoVO(
produtoId: $itemDTO->produtoId,
quantidade: new Quantidade($itemDTO->quantidade),
preco: new Moeda($itemDTO->precoCentavos)
);
}
return [
'clienteId' => $dto->clienteId,
'itens' => new ItensPedidoVO(...$itens)
];
}
}
5.2 Convertendo Value Objects para DTOs
<?php
class PedidoResponseMapper
{
public static function fromDominio(Pedido $pedido): PedidoResponseDTO
{
$itensDTO = [];
foreach ($pedido->itens() as $item) {
$itensDTO[] = new ItemResponseDTO(
produto: $item->produtoId(),
quantidade: $item->quantidade()->valor(),
subtotal: $item->subtotal()->formatado()
);
}
return new PedidoResponseDTO(
id: $pedido->id(),
cliente: $pedido->clienteNome(),
itens: $itensDTO,
total: $pedido->total()->formatado(),
status: $pedido->status()->value
);
}
}
6. Serialização e Persistência
6.1 Serialização para JSON
<?php
class Moeda implements \JsonSerializable
{
// ... implementação anterior ...
public function jsonSerialize(): mixed
{
return [
'centavos' => $this->centavos,
'formatado' => $this->formatado()
];
}
}
6.2 Persistindo com Doctrine ORM
<?php
/**
* @Embeddable
*/
class EnderecoEmbeddable
{
/** @Column(type="string") */
private string $logradouro;
/** @Column(type="string") */
private string $cidade;
/** @Column(type="string") */
private string $cep;
public function __construct(string $logradouro, string $cidade, string $cep)
{
$this->logradouro = $logradouro;
$this->cidade = $cidade;
$this->cep = $cep;
}
}
6.3 DTOs como Camada de Transporte
<?php
class PedidoService
{
public function __construct(
private PedidoRepository $repository
) {}
public function criar(CriarPedidoDTO $dto): PedidoResponseDTO
{
$dadosDominio = PedidoMapper::paraDominio($dto);
$pedido = new Pedido($dadosDominio['clienteId'], $dadosDominio['itens']);
$this->repository->salvar($pedido);
return PedidoResponseMapper::fromDominio($pedido);
}
}
7. Boas Práticas e Armadilhas Comuns
7.1 Evitando DTOs Anêmicos com Validação Vazada
Nunca coloque lógica de negócio em DTOs. A validação deve ocorrer antes da criação do DTO ou nos Value Objects durante a conversão.
7.2 Performance com Imutabilidade
Para coleções grandes, considere usar SplFixedArray ou bibliotecas especializadas. A clonagem constante pode ser custosa.
7.3 Regra Prática: VO vs DTO
- Use VO quando o valor tem regras de negócio e precisa ser validado
- Use DTO quando precisa transportar dados entre camadas sem comportamento
8. Exemplo Integrado: Sistema de Pedidos
8.1 Value Objects do Domínio
<?php
class Preco extends Moeda {} // Herda validação e imutabilidade
class Quantidade
{
public function __construct(private int $valor)
{
if ($valor <= 0) throw new \InvalidArgumentException("Quantidade deve ser positiva");
}
public function valor(): int { return $this->valor; }
}
class StatusPedido extends \MyProject\StatusPedido {} // Enum
8.2 DTOs da Aplicação
<?php
class CriarPedidoDTO
{
public function __construct(
public readonly string $clienteId,
public readonly array $itens // array de ItemPedidoDTO
) {}
}
class PedidoResponseDTO
{
public function __construct(
public readonly string $id,
public readonly string $cliente,
public readonly string $total,
public readonly string $status
) {}
}
8.3 Fluxo Completo
<?php
// Controller
class PedidoController
{
public function criar(Request $request): Response
{
$dto = CriarPedidoDTO::createFromArray($request->getBody());
$responseDTO = $this->pedidoService->criar($dto);
return response()->json($responseDTO);
}
}
// Service
class PedidoService
{
public function criar(CriarPedidoDTO $dto): PedidoResponseDTO
{
// Converte DTO para VOs
$itensVO = [];
foreach ($dto->itens as $item) {
$itensVO[] = new ItemPedidoVO(
new Preco($item['precoCentavos']),
new Quantidade($item['quantidade'])
);
}
// Cria entidade com VOs
$pedido = new Pedido(
clienteId: $dto->clienteId,
itens: new ItensPedidoVO(...$itensVO),
status: StatusPedido::PENDENTE
);
// Persiste
$this->repository->salvar($pedido);
// Retorna DTO
return new PedidoResponseDTO(
id: $pedido->id(),
cliente: $pedido->clienteNome(),
total: $pedido->total()->formatado(),
status: $pedido->status()->value
);
}
}
Referências
- Value Objects no Domain-Driven Design (Martin Fowler) — Artigo clássico definindo o padrão Value Object e suas características fundamentais
- Data Transfer Object (Patterns of Enterprise Application Architecture) — Descrição oficial do padrão DTO por Martin Fowler
- PHP 8.1: Propriedades readonly — Documentação oficial do PHP sobre propriedades readonly, essenciais para DTOs imutáveis
- Doctrine ORM: Embeddables — Guia oficial sobre como persistir Value Objects como Embeddables no Doctrine
- PHP Enums (PHP 8.1+) — Documentação oficial sobre Enums, úteis para Value Objects de estados finitos
- Refatoração: Replace Primitive with Object — Técnica de refatoração para substituir tipos primitivos por Value Objects
- DDD na Prática com PHP (Matheus Marabesi) — Tutorial prático em português sobre implementação de Value Objects em PHP