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