Building Maintainable PHP Applications: Value Objects
This article is part of the Building Maintainable PHP Applications series
Have you ever worked on a project where part of the service class or a command handler class had at least one or more validation checks before executing the main code that was supposed to be executed?
By validation checks I mean things like numbers less than 0 (example: calculating price or discount percentage), checking if a string is in a valid format (example: invoice number or invitation code) or maybe a number being in a specific range (example: maximum number of people in a team, latitude and longitude).
The first thing that most developers may think is that the validation for such business rules should be done as part of the request validation or that it’s fine to stay in the service class.
The problem is that such business rules may not always come from a usual http web request, there are other delivery mechanisms like
webhooks
console commands or cron jobs
a message queue such as Kafka or RabbitMQ
A 3rd party API response
which means these rules may need to be reused in multiple places depending on the scenario.
The other problem that I can see is that the code becomes more complex to manage and work with the more rules you add to the service or command handler.
Primitive values such as:
String
Boolean
Integer
Float
Double
only show us the type of the value that is being process but they don’t show us the correct format or if the value is valid by default.
We have to validate them every time, and if we are not careful, even overwrite them by mistake if we are not able to easily understand the code, meaning the values are easily mutable.
Let’s take a look at some Laravel code example of how it may look like:
1<?php 2 3class CalculateReservationPrice 4{ 5 public function __invoke(array $data) 6 { 7 $office = Office::findOrFail($data['id']); 8 9 $numberOfDays = Carbon::parse($data['end_date'])->endOfDay()->diffInDays(10 Carbon::parse($data['start_date'])->startOfDay()11 ) + 1;12 13 $price = $numberOfDays * $office->price_per_day;14 15 if ($numberOfDays >= 28 && $office->monthly_discount) {16 $price = $price - ($price * $office->monthly_discount / 100);17 }18 19 return $price;20 }21}
In short, the service class example here is trying to:
calculate the number of days
calculate the normal price
calculate the special price with an applied discount if the reservation is for more than or equal to 28 days and if the office has a monthly discount
If we focus more on the normal price calculation,
Is the price calculated in cents or in floats?
Is the price in dollars or in euros?
Is the price below zero?
Can the price be below zero?
Same questions can be asked for the discounted price based on the condition.
So what’s the solution? Value objects.
Value objects are objects that
Represent a value
Are immutable, meaning that they cannot be changed once instantiated,
Can represent one or multiple values
Validate the value before instantiating the object
Are instantiated via named constructors
Have methods that convert, compare or format the value
Do not have an identity
Let’s take a look at an example:
1<?php 2 3final readonly class Price 4{ 5 public function __construct(private int $value) 6 { 7 if ($value < 0) 8 { 9 throw new PriceCannotBeBelowZero();10 }11 }12 13 public static fromInteger(int $value): self14 {15 return self($value);16 }17 18 private function convertToDollars(): float19 {20 return $this->value / 100;21 }22 23 public function formattedWithSymbol(): string24 {25 return "$" . number_format($this->convertToDollars(), 2);26 }27 28 public function value(): int29 {30 return $this->value;31 }32}
As you can see in the example above:
The class has a named constructor - a static method named
fromInteger()
Has a dollar conversion method -
convertToDollars
Has validation logic in the constructor which checks if the value is below zero
Has a formatted version with the dollar sign in front -
formattedWithSymbol()
And this is how it is usually used in the code:
1<?php2 3$price = Price::fromInteger($price);4 5$price->formattedWithSymbol();6$price->value();
There are 2 types of value objects:
Simple - they represent a single value
Complex - they represent multiple values
The example above is a simple value object because it represents a single primitive value in US dollar cents.
If this was a complex value object, it would represent the currency as well:
1<?php 2 3declare(strict_types=1); 4 5final class Money 6{ 7 use MoneyFactory; 8 9 private string $amount;10 11 public function __construct(int|string $amount, private readonly Currency $currency)12 {13 if (filter_var($amount, FILTER_VALIDATE_INT) === false) {14 $numberFromString = Number::fromString((string) $amount);15 if (! $numberFromString->isInteger()) {16 throw new InvalidArgumentException('Amount must be an integer(ish) value');17 }18 19 $this->amount = $numberFromString->getIntegerPart();20 21 return;22 }23 24 $this->amount = (string) $amount;25 }26}
As you can see, the currency is also being passed to the constructor when it is instantiated so it would be used something like this:
1<?php2 3$fiveEur = Money::EUR(500);4$tenEur = $fiveEur->add($fiveEur);
The example is taken from the packagemoneyphp/money
(which you can use for money related calculations).
You can use value objects when you work with:
Money
Addresses
Email Addresses
Phone Numbers
Latitude and Longitude
Invitation Codes
Invoice Numbers
and many more
The only thing that you will differentiate the value objects from entities is that they don’t contain identifiers.
Now that you know a thing or two about value objects, you can see how some parts of the code from the service class can be moved to separate classes which can deal with the specifics of primitive values.