Value Objects in PHP

zero1.dev
5 min readApr 1, 2023

--

This medium post might have errors because I manually copied the content over here. To make sure you have the correct code, read the story at zero1.dev, click here.

One of the key concepts in Domain-Driven Design (DDD) is value objects (VOs). Value objects represent objects that don’t have an identity; instead, their equality depends on the content.

Examples of Value Objects

Money
Suppose we are building an e-commerce site. We can represent money as a value object because it doesn’t require identification. For example, if we have two $5 bills, they are the same because they both represent the same amount, $5. In the world of e-commerce, they are equal because they represent the same value. We don’t need to identify money with an ID or any other similar method.

Phone number
A phone number is another example that can be modeled using a value object. Like the money value object, a phone number is not identifiable and its equality depends on the content.

Currency
Currency is our last example. Like the previous two examples, there is no need to identify a currency. The equality between two currency objects depends on their content, most of the time their 3-letter ISO code.

Characteristics of Value Objects

Immutable
You must not alter the contents of a value object. Once it’s created, it should remain unchanged. To modify a value object, you need to create a new instance.

Validation
The beauty of value objects is that they can validate the data they receive before creating the object. For example, if you work with a currency value object, you can add validation criteria to accept only the USD currency.

Equality
For two value objects to be equal, all of their properties must be equal. However, in some cases, checking ‘all’ properties may be excessive. Therefore, it is up to you to decide which fields to use when checking the equality of two value objects.

Creating a currency Value Object

Based on what we said above, we will create a value object that represents a currency. To represent a currency, all we need is a 3-letter iso code.

We said that a value object must be immutable, thus the property must be declared as private. This ensures that the iso code property cannot be changed from the outside.

class Currency
{
public function __construct(private readonly string $isoCode)
{
}
}

The readonly keyword ensures that the property is initialized once, and further changes are not allowed. This makes it impossible to change the value of the property and guarantees immutability.

Next, we want to validate the ISO code, I want the ISO code to be either EUR or USD.

class Currency
{
public function __construct(private readonly string $isoCode)
{
if (!in_array($isoCode, ['EUR', 'USD'])) {
throw new InvalidArgumentException('Invalid ISO code');
}
}
}

The code now checks if the iso code is either EUR or USD, if it is not, it will throw an exception.

To complete the implementation of the currency value object, we will add a function that checks the equality of two currencies.

class Currency
{
public function __construct(private readonly string $isoCode)
{
if (!in_array($isoCode, ['EUR', 'USD'])) {
throw new InvalidArgumentException('Invalid ISO code');
}
}

public function isoCode(): string
{
return $this->isoCode;
}

public function equals(Currency $currency): bool
{
return $this->isoCode() === $currency->isoCode();
}
}

We added a getter to retrieve the ISO code, and we added a method that checks the equality of two currencies. As we mentioned above, for two value objects to be equal, *all* their properties must be equal. In this case, the only property we have is the ISO code and that’s the only thing we can check.

Using the Currency Value Object

$currencyInEur = new Currency('EUR');
echo $currencyInEur->isoCode(); // EUR
$currencyInUsd = new Currency('USD');
echo $currencyInUsd->isoCode(); // USD
echo $currencyInEur->equals($currencyInUsd); // false
new Currency('GBP'); // will throw an InvalidArgumentException

Building complex Value Objects

Let’s expand on the concept of value objects and create a Money Value Object. Money consists of a currency and an amount. Good thing, we already have a way to represent a currency 🙂. Let’s create a Money Value Object.

class Money
{
public function __construct(
private readonly Currency $currency,
private readonly int $amount,
)
{
}
public function currency(): Currency
{
return $this->currency;
}
public function amount(): int
{
return $this->amount;
}

public function equals(Money $money): bool
{
return $this->currency() === $money->currency() && $this->amount() === $money->amount();
}
}

There are 2 things to keep in mind here, first, to create the concept of Money, we need a currency, that’s why we used the Currency value object from earlier. Second, to check if two Money value objects are equal, we need to check their currency and amount. In the case of the Money value object, we didn’t have any form of validation. The currency is already validated in the Currency class and the amount is type hinted to an integer.

Finally, I want to include a **__toString** representation for the Money value object, so it returns a value such as **2000 EUR**.

class Money
{
public function __construct(
private readonly Currency $currency,
private readonly int $amount,
)
{
}

public function currency(): Currency
{
return $this->currency;
}

public function amount(): int
{
return $this->amount;
}
public function equals(Money $money): bool
{
return $this->currency()->equals($money->currency()) && $this->amount() === $money->amount();
}

public function __toString(): string
{
return "{$this->amount()} {$this->currency()->isoCode()}";
}
}

The **__toString()** method is a magic method that when used in a string context, PHP will return the result of the method.

Using the Money Value Object

$moneyEur = new Money(
new Currency('EUR'),
1200,
);
echo "An iPhone in Germany costs around $moneyEur"; // An iPhone in Germany costs around 1200 EUR
$moneyUsd = new Money(
new Currency('USD'),
1000,
);
echo "In the US it costs less, about $moneyUsd"; // In the US it costs less, about 1000 USD
echo $moneyEur->equals($moneyUsd); // false

Benefits of a Value Object

Thanks to Constructor Property Promotion and Readonly Properties, we were able to build a Currency and a Money value object using a few lines of code. By using a value object, we get the benefits of immutability, self-isolated data validation, and logic encapsulation. All those benefits help us create bug-free and easy-to-maintain code.

Final touch

Some might complain that the construction of complex Value Objects gets a bit out of hand. For that reason, we will refactor the construction of the objects as follows:

class Money
{
public function __construct(
private readonly Currency $currency,
private readonly int $amount,
)
{
}
....
public static function make(Currency $currency, int $amount): Money
{
return new self($currency, $amount);
}
}
class Currency
{
public function __construct(private readonly string $isoCode)
{
if (!in_array($isoCode, ['EUR', 'USD'])) {
throw new InvalidArgumentException('Invalid ISO code');
}
}
....
public static function make(string $isoCode): Currency
{
return new self($isoCode);
}
}

By adding a static method **make** in each class, the construction of the objects becomes simpler:

$moneyEur = Money::make(Currency::make('EUR'), 1200);
echo "An iPhone in Germany costs around $moneyEur"; // An iPhone in Germany costs around 1200 EUR
$moneyUsd = Money::make(Currency::make('USD'), 1000);
echo "In the US it costs less, about $moneyUsd"; // In the US it costs less, about 1000 USD

--

--