Data Transfer Objects (DTOs) and Value Objects (VOs) are particularly useful when working with dynamically typed languages like PHP, where switching to the use of more structured types is essential to improve the quality of our applications, as well as the readability of the code.
What is a DTO (Data Transfer Object)?
A DTO is an object that contains data and defines the structure of that data. The schema is defined by the names of the fields and their types.
It can only guarantee that all or some data are present, simply by relying on the rigidity of the programming language: if a constructor has a required string type parameter, you must pass a string to instantiate the object. However, a DTO does not provide any guarantee on the validity of that data: strings could be empty, integers could be negative, etc.
final class ADtoExample
{
public function __construct(
public readonly string $field,
// ...
) {
}
}
What is a VO (Value Object)?
A VO is an object that provides a guarantee of validity on the data it contains. They are immutable representations of data, aware of the specific domain business rules. They help us write more robust and readable code, minimizing bugs.
A value object is an object that represents a concept in the domain of an application. It is immutable, has no identity, and encapsulates a value.
final class AnVoExample
{
private function __construct(
private readonly string $value,
// ...
) {
}
public static function fromValue(
string $value
): self
{
match (true) {
empty($value) => throw new \InvalidArgumentException('value is empty'),
default => true
};
return new self($value);
}
}
A VO offers several advantages, including:
- Domain specificity: They can be used to represent domain-specific values. For example, the EmailAddress class can be used to represent only email addresses.
- Error reduction: They help to reduce errors because they encapsulate values and apply business rules.
- Better clarity: They make the code clearer and more understandable.
- Better testability: They are easier to test than primitive data types.
While a DTO deals with the structure of data, a VO also provides proof that the data meets expectations. When the VO class is used as a parameter, property, or return type, we know that we are dealing with a valid value.
How to use these types of objects?
DTO
A DTO should only be used in two places: where data enters the application or where data leaves the application. They help to encapsulate data, making it easier to manipulate and pass between different parts of the codebase.
Some examples:
- When a controller receives an HTTP POST request, the request data can be in any form: we can use a DTO to have the data with a known schema (keys and types verified).
- When we make an HTTP POST request to a web service: we can first collect the input data in a DTO and then serialize it into a request body that our HTTP client can send to the service.
- For queries: we can use a DTO to represent the query result.
- When we receive an HTTP GET request: we can first deserialize the API response into a DTO, so that we can apply a known schema to it instead of accessing the array keys directly and guessing the types.
VO
A VO is used whenever we want to verify that a value meets our expectations and do not want to verify it again.
They are particularly useful for representing the input and output data of an API. In this way, we can be sure that the data received from the API is valid and that the data sent to the API is correct.
We can also use it to create behaviors related to a particular value. For example, if we have a VO EmailAddress, we know that the value has already been verified to be a valid email address, so we do not need to check it again in other places. We can also add methods to the object that extract, for example, the username or hostname from the email address.
Let's take an example of an application where we need to manage prices. Without the use of VOs, our code might be full of functions that receive values (such as number amount and string currency) and each of them would have to repeat validation on the values to prevent errors. Skimping on these checks makes your code cleaner but introduces the risk of bugs. This is where VOs shine.
enum Currency : string
{
case Dollar = 'USD';
case Euro = 'EUR';
}
readonly class Price implements Stringable
{
public function __construct(
private float $amount,
private Currency $currency
)
{
match (true) {
($this->amount <= 0) => throw new InvalidPriceAmountException('amount is negative'),
default => true
};
}
public function __toString(): string
{
return number_format($this->amount, 2) . ' ' . $this->currency->value;
}
public function getAmount(): float
{
return $this->amount;
}
public function getCurrency(): string
{
return $this->currency->value;
}
}
$price = new Price(15, '$'); // Invalid Argument Exception
$price = new Price(0, Currency::Dollar); // InvalidPriceAmountException
$price = new Price(29.99, Currency::Dollar); // OK
echo $price; // 29.99 USD
Every time you want to represent a price in your code, you can create a Price VO and trust the internal validation performed during instantiation. This means that for the lifecycle of that object in your code, you don't need to revalidate it wherever it's passed between functions.
Conclusions
In conclusion, DTO and VO are two design patterns that are useful for improving code quality and readability in PHP.
DTOs are objects that contain data and define the structure of that data. They are useful for transferring data between different parts of an application.
VOs are objects that provide assurance of data validity. They are useful for representing domain-specific values and applying business rules.
Good work! 👨💻
Top comments (5)
omg dude why are you checking things like this
this is crazy 😭 just use
if
VO example?
I asked GPT for that:
gist.github.com/graceman9/4335154c...
This is not a VO, at all.
Yes, you are right! But I did some research and I think I finally got the concept of VO. If someone interested I think this is a good explanation:
dev.to/ianrodrigues/writing-value-...