Newsletter

Value objects in PHP: immutability, equality, validation

Value objects in PHP: immutability, equality, validation

A value object in PHP is a type that wraps data and is distinguished only by its properties. Unlike an Entity, it has no unique identifier. Two value objects in PHP holding the same property values must therefore be treated as equal.

Good candidates for value objects are:

  • a price
  • a phone number
  • an address
  • a commit hash
  • an entity identifier
  • and so on.

When you design a value object in PHP, pay attention to its three main characteristics: immutability, structural equality and self-validation.

A value object example in PHP

php
final class Price
{
    const EU = 'EU';
    const CAD = 'CAD';

/** @var float */
    private $amount;

/** @var string */
    private $currency;

public function __construct(float $amount, string $currency = 'EU')
    {
        if ($amount < 0) {
            throw new InvalidArgumentException("Le montant doit être une valeur positive: {$amount}.");
        }

if (!in_array($currency, $this->getAvailableCurrencies())) {
            throw new InvalidArgumentException("La devise doit être valide: {$currency}.");
        }

$this->amount = $amount;
        $this->currency = $currency;
    }

private function getAvailableCurrencies(): array
    {
        return [self::EU, self::CAD];
    }

public function getAmount(): float
    {
        return $this->amount;
    }

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

Immutability

Once you have instantiated a value object in PHP, it has to stay the same for the rest of the application’s lifetime. If you need to change its value, you replace the object entirely.

Mutable value objects are acceptable as long as you keep them entirely within a local scope, with a single reference to the object. Anywhere else, you are asking for trouble.

Going back to the previous example, here is how to update the amount of a price type:

php
final class Price
{
    // ...

private function hasSameCurrency(Price $price): bool
    {
        return $this->currency === $price->currency;
    }

public function sum(Price $price): self
    {
        if (!$this->hasSameCurrency($price)) {
            throw InvalidArgumentException(
                "Vous ne pouvez additionner que les valeurs ayant la même devise: {$this->currency} !== {$price->currency}."
            );
        }

return new self($this->amount + $price->amount, $this->currency);
    }
}

Structural equality

Value objects in PHP have no identifier. In other words, two value objects holding the same internal values have to be treated as equal. Since PHP cannot overload the equality operator, you have to implement the comparison yourself.

A dedicated method does the job:

php
final class Price
{
    // ...

public function isEqualsTo(Price $price): bool
    {
        return $this->amount === $price->amount &&
        $this->currency === $price->currency;
    }
}

Self-validation

A value object is validated when it is created. If one of its properties is invalid, an exception is thrown. Add immutability to that and, once a value object exists, you can be sure it will always be valid.

Back to the Price type: a negative amount makes no sense in the application domain:

php
final class Price
{
    // ...

public function __construct(float $amount, string $currency = 'USD')
    {
        if ($amount < 0) {
            throw new InvalidArgumentException("Le montant doit être une valeur positive: {$amount}.");
        }

if (!in_array($currency, $this->getAvailableCurrencies())) {
            throw new InvalidArgumentException("La devise doit être valide: {$currency}.");
        }

$this->amount = $amount;
        $this->currency = $currency;
    }
}

Conclusion

Value objects are a good way to write clean code. Instead of writing:

php
public function addPhoneNumber(string $phone) : void {}

You can write:

php
public function addPhoneNumber(string $phone) : void {}

That is easier to read and to reason about, and you no longer have to work out which phone format to use. Since their attributes are what define them, and since you can share them across different entities, they can be cached forever.

They also cut duplication. Instead of multiple amount and currency fields, you use a single price class.

Of course, like everything in life, value objects in PHP can be overdone. Picture yourself converting piles of value objects to store them in the database, then converting them back when you read them out again: you are heading straight for performance problems. And a codebase full of value objects gets heavy.

Use them for a field, or a group of fields, in your domain that needs validation, or to remove an ambiguity, phone number formats being the obvious case.

PHP

Damien Flandrin Web developer since 2010, creator of Gekkode and Email Impact. Every article is tested on a real project before publication. Contact
Newsletter

New tests, tutorials and projects, by e-mail.

Reproducible tests, versioned code, dated results. Never any spam.