← Back to articles
PHP Architecture & Design Patterns

DTO vs Value Object: Untangling the Confusion in PHP

A clear guide to the difference between DTOs and Value Objects in PHP: their purpose, behavior, validation, and when to use each in a clean architecture.

✦
Featured article ↗

DTO vs Value Object: Untangling the Confusion in PHP

If you've spent any time in modern PHP circles, you've heard both terms: DTO and Value Object. They sound similar, they're both simple classes, and they both hold data. Developers often use them interchangeably — and that's a mistake that leads to muddled architecture.

In this article, we'll untangle the confusion once and for all. You'll learn what each pattern is for, how they differ, and — most importantly — how to decide which one you actually need in a given situation.

The Root of the Confusion

Both DTOs and Value Objects share several characteristics that make them easy to confuse:

  • They are both small classes.
  • They both hold data.
  • They are often immutable.
  • They may look nearly identical in simple cases.

But they exist for entirely different reasons. Confusing them is like confusing a shipping box with a coin — both are objects, but their purpose, rules, and lifecycle are completely different.

What Is a DTO?

A Data Transfer Object is a class whose job is to carry data between layers or processes. That's it. It has no behavior beyond getting and setting values (or exposing readonly properties).

Typical use cases:

  • Returning data from a controller to a view.
  • Passing input from an HTTP request to a service.
  • Serializing data to JSON for an API response.
  • Mapping database rows to an application-friendly structure.

A simple DTO looks like this:

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

Note what's missing: no validation, no business rules, no domain meaning. It's a container. Nothing more.

What Is a Value Object?

A Value Object is a class that represents a concept from your domain. It is defined entirely by its values, has no identity, and — crucially — it enforces its own validity and encapsulates behavior related to those values.

Typical use cases:

  • Representing an Email, Money, UserId, PhoneNumber, or DateRange.
  • Guaranteeing that a value is always valid wherever it's used.
  • Encapsulating domain rules like currency matching or date overlaps.
  • Eliminating primitive obsession in your domain model.

A simple Value Object looks like this:

<?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; } }

Notice what's present here: validation, normalization, equality, and intent. The object is not just holding data — it is a concept.

Side-by-Side Comparison

  • Purpose: DTO — transport data; VO — represent a domain concept.
  • Behavior: DTO — none (only getters/setters or readonly props); VO — may contain domain behavior.
  • Validation: DTO — validated externally (e.g., in a form request); VO — validates itself on creation.
  • Identity: DTO — irrelevant; VO — defined by values, no identity.
  • Equality: DTO — usually not implemented; VO — equality based on values, often via equals().
  • Immutability: DTO — optional; VO — required.
  • Layer: DTO — application layer (boundaries); VO — domain layer.
  • Source of truth: DTO — external data; VO — domain rules.

A Concrete Example

Imagine you're building a user registration feature. You receive an HTTP request with raw input. This is where a DTO shines:

<?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'], ); } }

Once validation passes, you convert the DTO into a domain object that uses 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), );

See the flow? The DTO carried raw data across the application boundary. The Value Objects protected the domain from invalid state. Each pattern did its job — and neither could replace the other.

Where People Go Wrong

Confusion between DTOs and VOs typically leads to one of two mistakes:

  1. Using a DTO as a Value Object. You end up with validation logic scattered in services because the DTO doesn't validate. The domain is left unprotected.
  2. Using a Value Object as a DTO. You force domain concepts into transport concerns — adding serialization methods, "toArray()", or extra constructors — polluting your domain with infrastructure logic.

Both mistakes erode the boundaries that clean architecture depends on.

How to Decide: A Simple Heuristic

Ask yourself: does this class represent a concept with rules, or does it just carry data?

  • If it represents a domain concept (email, money, order id) → Value Object.
  • If it carries data between layers (form input, API payload, query result) → DTO.

Another way to think about it:

  • A DTO is a shape — it defines what fields exist.
  • A Value Object is a meaning — it defines what the value is and what it can do.

Can a DTO Contain Value Objects?

Yes, and this is often a good idea. A DTO might carry Value Objects instead of primitives when the boundary calls for it:

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

This is fine as long as the DTO doesn't add behavior and the Value Objects keep enforcing their rules. The DTO still transports; the VOs still protect.

Can a Value Object Be Used Directly in Serialization?

In many applications, Value Objects are serialized to JSON via custom normalizers or by exposing a toString() or toNative() method. That's acceptable — but avoid mixing persistence or transport concerns into the VO's core responsibilities. If serialization logic becomes heavy, put it in a separate serializer rather than inside the VO itself.

Best Practices

  • Keep DTOs boring. No validation, no business rules, no hidden magic. Just data and constructors.
  • Keep Value Objects strict. Validate on creation, enforce invariants, provide useful behavior.
  • Don't blur the boundaries. Convert DTOs into domain objects at the edge of your application.
  • Choose immutability everywhere. Both patterns benefit from readonly and constructor promotion in PHP 8.
  • Let the domain speak. Wherever a primitive could carry meaning, replace it with a Value Object.

Conclusion

DTOs and Value Objects are both simple, powerful, and often confused. The distinction is not academic — it's architectural. A DTO is a courier; a Value Object is a citizen of your domain. One moves data, the other enforces meaning.

Use DTOs at the boundaries of your application to carry data in and out. Use Value Objects inside your domain to make invalid states impossible and to express business rules clearly. When you keep these roles separate, your codebase becomes easier to test, easier to reason about, and much harder to break.

Don't treat them as interchangeable. Treat them as complementary — and your architecture will thank you.

Technologies & topics

Article tags

No articles match these filters.

Have a project or an idea to discuss?

Let's talk ↗