← Volver a los artículos
Arquitectura PHP y patrones de diseño

DTO vs Value Object: desenredando la confusión en PHP

Una guía clara sobre la diferencia entre DTO y Value Objects en PHP: su propósito, comportamiento, validación y cuándo usar cada uno en una arquitectura limpia.

✦
Artículo destacado ↗

DTO vs Value Object: desenredando la confusión en PHP

Si has pasado algún tiempo en los círculos modernos de PHP, has escuchado ambos términos: DTO y Value Object. Suenan similares, ambos son clases simples, y ambos contienen datos. Los desarrolladores a menudo los usan de forma intercambiable — y eso es un error que conduce a una arquitectura confusa.

En este artículo desenredaremos la confusión de una vez por todas. Aprenderás para qué sirve cada patrón, en qué se diferencian y — lo más importante — cómo decidir cuál necesitas realmente en una situación determinada.

La raíz de la confusión

Tanto los DTO como los Value Objects comparten varias características que los hacen fáciles de confundir:

  • Ambos son clases pequeñas.
  • Ambos contienen datos.
  • A menudo son inmutables.
  • En casos simples pueden parecer casi idénticos.

Pero existen por razones completamente diferentes. Confundirlos es como confundir una caja de envío con una moneda — ambos son objetos, pero su propósito, reglas y ciclo de vida son completamente diferentes.

¿Qué es un DTO?

Un Data Transfer Object es una clase cuyo trabajo es transportar datos entre capas o procesos. Eso es todo. No tiene comportamiento más allá de obtener y establecer valores (o exponer propiedades readonly).

Casos de uso típicos:

  • Devolver datos de un controlador a una vista.
  • Pasar la entrada de una petición HTTP a un servicio.
  • Serializar datos a JSON para una respuesta API.
  • Mapear filas de base de datos a una estructura amigable para la aplicación.

Un DTO simple se ve así:

<?php declare(strict_types=1); final class CreateUserDTO { public function __construct( public readonly string $name, public readonly string $email, public readonly int $age, ) { } }

Observa lo que falta: sin validación, sin reglas de negocio, sin significado de dominio. Es un contenedor. Nada más.

¿Qué es un Value Object?

Un Value Object es una clase que representa un concepto de tu dominio. Está definido enteramente por sus valores, no tiene identidad y — crucialmente — impone su propia validez y encapsula el comportamiento relacionado con esos valores.

Casos de uso típicos:

  • Representar un Email, Money, UserId, PhoneNumber o DateRange.
  • Garantizar que un valor sea siempre válido dondequiera que se use.
  • Encapsular reglas de dominio como la coincidencia de monedas o las superposiciones de fechas.
  • Eliminar la obsesión por las primitivas en tu modelo de dominio.

Un Value Object simple se ve así:

<?php declare(strict_types=1); final class Email { private function __construct( private readonly string $value, ) { } public static function fromString(string $value): self { $normalized = mb_strtolower(trim($value)); if (!filter_var($normalized, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException( sprintf('"%s" is not a valid email address.', $value) ); } return new self($normalized); } public function toString(): string { return $this->value; } public function equals(self $other): bool { return $this->value === $other->value; } }

Observa lo que está presente aquí: validación, normalización, igualdad e intención. El objeto no solo contiene datos — es un concepto.

Comparación lado a lado

  • Propósito: DTO — transportar datos; VO — representar un concepto del dominio.
  • Comportamiento: DTO — ninguno (solo getters/setters o props readonly); VO — puede contener comportamiento de dominio.
  • Validación: DTO — validado externamente (por ejemplo, en un form request); VO — se autovalida al crearse.
  • Identidad: DTO — irrelevante; VO — definido por valores, sin identidad.
  • Igualdad: DTO — generalmente no implementada; VO — igualdad basada en valores, a menudo vía equals().
  • Inmutabilidad: DTO — opcional; VO — obligatoria.
  • Capa: DTO — capa de aplicación (fronteras); VO — capa de dominio.
  • Fuente de verdad: DTO — datos externos; VO — reglas de dominio.

Un ejemplo concreto

Imagina que estás construyendo una funcionalidad de registro de usuario. Recibes una petición HTTP con datos crudos. Aquí es donde un DTO brilla:

<?php final class RegisterUserRequest { public function __construct( public readonly string $name, public readonly string $email, public readonly int $age, ) { } public static function fromArray(array $data): self { return new self( name: $data['name'], email: $data['email'], age: (int) $data['age'], ); } }

Una vez que pasa la validación, conviertes el DTO en un objeto de dominio que usa Value Objects:

<?php final class User { public function __construct( public readonly UserId $id, public readonly Name $name, public readonly Email $email, public readonly Age $age, ) { } } $user = new User( id: UserId::generate(), name: Name::fromString($dto->name), email: Email::fromString($dto->email), age: Age::fromInt($dto->age), );

¿Ves el flujo? El DTO transportó datos crudos a través de la frontera de la aplicación. Los Value Objects protegieron el dominio de estados inválidos. Cada patrón hizo su trabajo — y ninguno podía reemplazar al otro.

Dónde se equivoca la gente

La confusión entre DTO y VO típicamente conduce a uno de dos errores:

  1. Usar un DTO como Value Object. Terminas con lógica de validación dispersa en los servicios porque el DTO no valida. El dominio queda sin protección.
  2. Usar un Value Object como DTO. Fuerzas conceptos del dominio a asumir preocupaciones de transporte — añadiendo métodos de serialización, «toArray()» o constructores extra — contaminando tu dominio con lógica de infraestructura.

Ambos errores erosionan las fronteras de las que depende la arquitectura limpia.

Cómo decidir: una heurística simple

Pregúntate: ¿esta clase representa un concepto con reglas, o simplemente transporta datos?

  • Si representa un concepto del dominio (email, money, order id) → Value Object.
  • Si transporta datos entre capas (entrada de formulario, payload API, resultado de consulta) → DTO.

Otra forma de pensarlo:

  • Un DTO es una forma — define qué campos existen.
  • Un Value Object es un significado — define qué es el valor y qué puede hacer.

¿Puede un DTO contener Value Objects?

Sí, y a menudo es una buena idea. Un DTO puede transportar Value Objects en lugar de primitivas cuando la frontera lo requiere:

<?php final class UpdateUserProfileDTO { public function __construct( public readonly UserId $userId, public readonly ?Name $name, public readonly ?Email $email, ) { } }

Esto está bien siempre que el DTO no añada comportamiento y los Value Objects sigan imponiendo sus reglas. El DTO sigue transportando; los VO siguen protegiendo.

¿Puede un Value Object usarse directamente en la serialización?

En muchas aplicaciones, los Value Objects se serializan a JSON mediante normalizadores personalizados o exponiendo un método toString() o toNative(). Eso es aceptable — pero evita mezclar preocupaciones de persistencia o transporte con las responsabilidades principales del VO. Si la lógica de serialización se vuelve pesada, colócala en un serializador separado en lugar de dentro del propio VO.

Mejores prácticas

  • Mantén los DTO aburridos. Sin validación, sin reglas de negocio, sin magia oculta. Solo datos y constructores.
  • Mantén los Value Objects estrictos. Valida al crear, impone invariantes, proporciona comportamiento útil.
  • No difumines las fronteras. Convierte los DTO en objetos de dominio en el borde de tu aplicación.
  • Elige la inmutabilidad en todas partes. Ambos patrones se benefician de readonly y la promoción de constructor en PHP 8.
  • Deja que el dominio hable. Dondequiera que una primitiva pueda llevar significado, reemplázala con un Value Object.

Conclusión

Los DTO y los Value Objects son ambos simples, poderosos y a menudo confundidos. La distinción no es académica — es arquitectónica. Un DTO es un mensajero; un Value Object es un ciudadano de tu dominio. Uno mueve datos, el otro impone significado.

Usa DTO en las fronteras de tu aplicación para transportar datos hacia dentro y hacia fuera. Usa Value Objects dentro de tu dominio para hacer imposibles los estados inválidos y expresar claramente las reglas de negocio. Cuando mantienes estos roles separados, tu base de código se vuelve más fácil de testear, más fácil de razonar y mucho más difícil de romper.

No los trates como intercambiables. Trátalos como complementarios — y tu arquitectura te lo agradecerá.

Tecnologías y temas

Etiquetas del artículo

No hay artículos que coincidan con estos filtros.

¿Tienes un proyecto o una idea para discutir?

Hablemos ↗