Value Object
When programming, we often have to deal with two or more pieces of information that belong closely together. Examples of this are 8 meters, 3 liters, 4 kilograms or 12.50 euros. Of course, we could represent such values with individual scalar variables:
<?php declare(strict_types=1);
$amount = 200;
$currency = 'EUR';
do_something($amount, $currency);
function do_something(int $amount, string $currency): void
{
// ...
}
However, this can easily go wrong because related information is represented by independent variables. We could easily get things mixed up:
<?php declare(strict_types=1);
$amount1 = 200;
$currency1 = 'EUR';
$amount2 = 200;
$currency2 = 'GBP';
do_something($amount1, $currency2);
function do_something(int $amount, string $currency): void
{
// ...
}
PHP does not support developer-defined data structures. We could therefore combine the two variables in an associative array. This could look something like this:
<?php declare(strict_types=1);
$money = [
'amount' => 200,
'currency' => 'EUR'
];
However, if we want to work cleanly, we would have to check whether the corresponding key even exists in the array each time it is accessed, as otherwise we would produce a hint and continue working with a null value rather unexpectedly.
Even worse, however, is that anyone can not only read but also write and thus change all values at any time. This is a bit like the problem with global variables: it gets out of control far too quickly.
The best solution is therefore to combine the two variables in one object. Because this object represents a specialized value, it is also called a value object.
<?php declare(strict_types=1);
class Money
{
public function __construct(
private int $amount,
private string $currency
) {}
}
Now, however, we have to dig into a relatively technical detail to understand a major problem with our solution.
Values and references
When calling methods or functions, PHP distinguishes between the parameter transfer in the call by value and the call by reference method. For scalar values, a copy of the transferred data is created in the local scope of the function or method when it is called. If objects are passed as parameters, PHP works with references. This enables programming with side effects, namely when the state of the passed objects is changed by method calls.
Digression: What I have described here is the visible behavior of PHP. Behind the scenes, PHP works differently, namely also with call by value with (internal) references and reference counters. This means that PHP is much smarter than we think, because a copy of data is only actually created if it is also changed in the local scope. This is called copy on write, by the way. Incidentally, this is also the reason why you don't want to work explicitly with references in PHP, apart from a few exceptional cases.
If we now use an object or a class to represent a technical value consisting of two or more pieces of information, then we have a problem with the semantics of the call, because a value wants to be copied, while the object is passed by reference.
Miraculous money multiplication
This leads to the funny problem of miraculous money multiplication. Let's imagine an (incorrectly implemented) value object that represents money. The object has theamount
and thecurrency
as members. We represent the amount as an integer in cents to avoid rounding errors.
<?php declare(strict_types=1);
class Money
{
public function __construct(
private int $amount,
private string $currency
) {}
public function increase(int $amount): void
{
$this->amount += $amount;
}
}
$myMoney = new Money(200, 'EUR');
$yourMoney = $myMoney;
// ...
$yourMoney->increase(100);
var_dump($myMoney, $yourMoney);
Of course, this object is not suitable for practical use because it lacks any plausibility checks. But that's not the point. I first generate 20 EUR($myMoney
). Then I give you this 20 EUR ($yourMoney
). A while later, perhaps because you speculated on the stock market, you have more money and therefore have to increase the $amount
(increase()
):
mutable.php:23: class Money#1 (2) { private int $amount => int(300) private string $currency => string(3) "EUR" } mutable.php:23: class Money#1 (2) { private int $amount => int(300) private string $currency => string(3) "EUR" }
But what has happened now? I got rich, all by myself. This is the miraculous increase in money I talked about above. A great thing, isn't it?
Seriously: of course it doesn't work like that. If we want to look at it from a technical point of view, then after my gift we're both still holding the EUR 20 bill in our hands. We can see this from the fact that var_dump()
shows us #1
for both objects, which means that both $myMoney
and $yourMoney
reference the identical object.
But this is only the first problem. It gets much worse when you increase your money by calling the increase()
method. This also changes the amount that I have in my hand. Okay, it would only be really good for me if the object had a withdraw()
method ...
If we only used scalar values instead of an object, we wouldn't have a problem here:
<?php declare(strict_types=1);
$myMoney = 200;
$yourMoney = $myMoney;
// ...
$yourMoney += 100;
var_dump($myMoney, $yourMoney);
scalar.php:10: int(200) scalar.php:10: int(300)
But now we are back to the fact that amount and currency belong closely together, but are managed as separate variables. This is error-prone, because it can easily happen that we mix an amount with the wrong currency. Or, because global variables in PHP are not type-safe, we can write completely nonsensical values somewhere.
So we have to combine the best of both worlds: call by value semantics and working with objects.
Immutability
Since we cannot change the way PHP works with objects internally, we must prohibit or make it impossible to change the state of our value object. Such an object is called immutable. Such an immutable object is initialized by the constructor and may then no longer change its state. In current PHP versions, we can map this very nicely using public read-only properties
:
<?php declare(strict_types=1);
class Money
{
public function __construct(
public readonly int $amount,
public readonly string $currency
) {}
}
$money = new Money(200, 'EUR');
var_dump($money->amount, $money->currency);
$money->amount += 300;
Anyone can read here, but it is not possible to change it:
immutable-will-fail.php:13: int(200) immutable-will-fail.php:13: string(3) "EUR" PHP Fatal error: Uncaught Error: Cannot modify readonly property Money::$amount in immutable-will-fail.php:15 Stack trace: #0 {main} thrown in immutable-will-fail.php on line 15
But what do we do now with our increase()
method? We let it return a new instance of Money
. Thus, the original object remains unchanged (and thus immutable) and we have another, newly created object that represents the changed value:
<?php declare(strict_types=1);
class Money
{
public function __construct(
public readonly int $amount,
public readonly string $currency
) {}
public function increase(int $amount): self
{
$this->amount += $amount;
}
}
$money = new Money(200, 'EUR');
var_dump($money->amount, $money->currency);
We use self
as the return type, which is resolved to Money
by the compiler.
money.php:18: int(200) money.php:18: string(3) "EUR"
Our value objects are therefore basically disposable objects. We create as many instances of them as we need. The garbage collection cleans up the objects as soon as they are no longer referenced. The creation of a money object
is not complex and is therefore very quick.
Even more value objects
The value object is one of the most important design patterns that is used far too little by most developers. Instead, there are far too many entities because their definition states that an entity represents an object with identity. Since every record that comes from a database has a primary key and therefore an identifier, many developers conclude that more or less everything that has ever been loaded from a database is an entity. ORMs send their regards.
In many use cases, the identity of a thing is not important. In everyday life, for example, we are not interested in the identity of a 20 euro bill; in fact, we are probably not even interested in whether we have a 20 euro bill or two 10 euro bills in our hands. A central bank, on the other hand, will be very interested in the identifier of a banknote, namely the serial number. In this context, the banknote should be represented as an entity, normally a value object will suffice.
A value object is not a DTO because it contains functionality. For example, we want to ensure that we do not add different currencies:
<?php declare(strict_types=1);
$money = new Money(200, 'EUR');
$sum = $money->add(new Money(100, 'EUR'));
var_dump($sum);
class Money
{
public function __construct(
private int $amount,
private string $currency
) {}
public function add(self $that): self
{
$this->ensureSameCurrency($that);
return new self($this->amount + $that->amount, $this->currency);
}
private function ensureSameCurrency(self $that): void
{
if ($this->currency !== $that->currency) {
throw new MoneyException('Currency mismatch');
};
}
}
class MoneyException extends RuntimeException
{
}
If we add the same currencies, everything works fine:
Money.php:6: class Money#3 (2) { private int $amount => int(300) private string $currency => string(3) "EUR" }
Adding different currencies, on the other hand, leads to an exception:
<?php declare(strict_types=1);
$money = new Money(200, 'EUR');
$money->add(new Money(100, 'GBP'));
class Money
{
public function __construct(
private int $amount,
private string $currency
) {}
public function add(self $that): self
{
$this->ensureSameCurrency($that);
return new self($this->amount + $that->amount, $this->currency);
}
private function ensureSameCurrency(self $that): void
{
if ($this->currency !== $that->currency) {
throw new MoneyException('Currency mismatch');
};
}
}
class MoneyException extends RuntimeException
{
}
PHP Fatal error: Uncaught MoneyException: Currency mismatch in Money-will-fail.php:23 Stack trace: #0 Money-will-fail.php(15): Money->ensureSameCurrency() #1 Money-will-fail.php(4): Money->add() #2 {main} thrown in Money-will-fail.php on line 23
Of course, a Money object
actually has even more methods, and we would not represent the currency as a string
, but presumably as an enum
. But that is not the focus here.
Value objects usually have an equals()
method that can be used to compare them.
Sometimes we create value objects based on a single piece of scalar information. This is always useful if there are plausibility checks or restrictions in the value range that must be fulfilled. Instead of either blindly relying on someone having validated data in advance or alternatively repeating any checks before each use of a scalar value, you can then create a self-validating object and pass it around. Since value objects are always immutable, it is in the nature of things that the data once validated in the constructor can never change again. We are therefore dealing here with an object that guarantees data quality.