← Back to articles
PHP Architecture & Design Patterns

DTO vs Value Object: Untangling the Confusion in PHP

How often have you heard: "It's just data, why overcomplicate it?" This often leads to magic numbers in arrays, invalid states, and bugs that are hard to trace. These are common consequences of mixing up Data Transfer Objects (DTOs) and Value Objects (VOs). Let's break down their fundamental differ...

Featured article
How often have you heard: "It's just data, why overcomplicate it?" This often leads to magic numbers in arrays, invalid states, and bugs that are hard to trace. These are common consequences of mixing up Data Transfer Objects (DTOs) and Value Objects (VOs). Let's break down their fundamental differences, when to use each, and illustrate with clear PHP examples. DTO (Data Transfer Object): The Simple Messenger The primary goal of a DTO is to carry data. It's a simple container that gathers information from one place (like a database or an API request) and transfers it to another (like a service, controller, or the frontend). Key Characteristics of a DTO in PHP: Data without Behavior: It's a bag of public properties or properties with getters/setters. It contains no business logic. Mutable: Data within a DTO can typically be changed after creation. Identity by Structure: Two DTOs with the same data are considered the same, but they are usually not compared; they just "carry" data. No Life Cycle: It's created, used, and discarded. Data without Behavior: It's a bag of public properties or properties with getters/setters. It contains no business logic. Mutable: Data within a DTO can typically be changed after creation. Identity by Structure: Two DTOs with the same data are considered the same, but they are usually not compared; they just "carry" data. No Life Cycle: It's created, used, and discarded. A Simple DTO Example: class UserRegistrationDto { public function __construct( public string $email, public string $plainPassword, public string $firstName, public string $lastName ) {} } // Usage: we get a request, create a DTO. $dto = new UserRegistrationDto( email: $_POST['email'], plainPassword: $_POST['password'], firstName: $_POST['first_name'], lastName: $_POST['last_name'] ); // We pass the DTO to a service for processing. $userService->register($dto); ➡️ When to use a DTO? To transfer data between application layers (Controller -> Service, Service -> Repository). To shape an API response. When you need to group many parameters into a single object. To transfer data between application layers (Controller -> Service, Service -> Repository). To shape an API response. When you need to group many parameters into a single object. Value Object (VO): The Immutable Expert The primary goal of a VO is to represent a domain concept with its own rules and integrity. It's not just data; it's data with behavior that guarantees its correctness. Key Characteristics of a VO in PHP: Immutability: This is the most important rule! Once created, its internal state cannot be changed. Any "modification" returns a new object instance. Has Behavior: A VO contains methods to work with its value. Identity by Value: Two VOs with the same internal state are considered identical and interchangeable. Self-Validation: It knows what a valid state is and enforces it upon creation. Immutability: This is the most important rule! Once created, its internal state cannot be changed. Any "modification" returns a new object instance. Has Behavior: A VO contains methods to work with its value. Identity by Value: Two VOs with the same internal state are considered identical and interchangeable. Self-Validation: It knows what a valid state is and enforces it upon creation. A Simple VO Example – Email: class Email implements \Stringable { private function __construct(private string $value) { // Validation on creation! An invalid Email cannot exist. if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException("Invalid email address: '{$value}'"); } } // A static constructor is a common practice for VOs. public static function fromString(string $value): self { return new self($value); } // A getter, not a setter! The value cannot be changed after creation. public function toString(): string { return $this->value; } public function __toString(): string { return $this->toString(); } // Comparison by value. public function equals(self $other): bool { return $this->value === $other->value; } // Example of behavior: getting the email domain. public function getDomain(): string { return explode('@', $this->value)[1]; } } // Usage: try { $email = Email::fromString('user@example.com'); // $email->value = 'hack'; // Impossible! The property is private. $newEmail = Email::fromString('new@domain.com'); // This is a new object. // We can pass it to a User entity, confident that the email is valid. $user = new User($email); } catch (\InvalidArgumentException $e) { // Handle validation error. } ➡️ When to use a Value Object? When data has specific validation rules (Email, PhoneNumber, Money, Coordinate). When you want to eliminate primitive obsession and make your code more expressive and type-safe. When a value is logically a whole concept (e.g., Money consists of an amount and a currency, which are inseparable). In the Domain Core according to Domain-Driven Design (DDD). When data has specific validation rules (Email, PhoneNumber, Money, Coordinate). When you want to eliminate primitive obsession and make your code more expressive and type-safe. When a value is logically a whole concept (e.g., Money consists of an amount and a currency, which are inseparable). In the Domain Core according to Domain-Driven Design (DDD). Summary: A Developer's Cheat Sheet Final Takeaway and Key Advice Do you just need to move a packet of data from point A to point B? Use a DTO. Do you want to express a domain concept and guarantee data integrity throughout its life cycle? Create a Value Object. Do you just need to move a packet of data from point A to point B? Use a DTO. Do you want to express a domain concept and guarantee data integrity throughout its life cycle? Create a Value Object. Start small: replace plain strings for emails in your code with an Email Value Object. You will immediately feel how much cleaner, more reliable, and more understandable your code becomes. How do you use DTOs and Value Objects in your projects? Have you faced issues due to confusing them? Share your experiences in the comments below! #PHP #OOP #SoftwareArchitecture #DDD #DomainDrivenDesign #BackendDevelopment #CleanCode #ProgrammingTips #SoftwareDevelopment #CodeQuality #DataStructures #API #DTO #Value Object #VO
Technologies & topics

Article tags

No projects match these filters.

Have a project or an idea to discuss?

Let's talk ↗