Value objects in PHP: onveranderlijkheid, gelijkheid, validatie

Value objects in PHP: onveranderlijkheid, gelijkheid, validatie

Een value object in PHP is een type dat data omsluit en zich uitsluitend onderscheidt door zijn eigenschappen. Anders dan een entiteit heeft het geen unieke identifier. Twee value objects in PHP met dezelfde waarden voor hun eigenschappen moeten dus als gelijk worden beschouwd.

Goede kandidaten voor een value object zijn:

  • een prijs
  • een telefoonnummer
  • een adres
  • een commit-hash
  • de identifier van een entiteit
  • enzovoort.

Let bij het ontwerp van een value object in PHP op de drie belangrijkste eigenschappen: onveranderlijkheid, structurele gelijkheid en zelfvalidatie.

Voorbeeld van een value object 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;
    }
}

Onveranderlijkheid

Zodra je een value object in PHP hebt geïnstantieerd, moet het de rest van de levensduur van de applicatie hetzelfde blijven. Moet je de waarde wijzigen, dan vervang je het object volledig.

Een muteerbaar value object gebruiken kan, zolang je het volledig binnen een lokale scope houdt, met één enkele referentie naar het object. Doe je dat niet, dan loop je tegen problemen aan.

Terug naar het vorige voorbeeld: zo werk je het bedrag van een prijstype bij.

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);
    }
}

Structurele gelijkheid

Value objects in PHP hebben geen identifier. Met andere woorden: hebben twee value objects dezelfde interne waarden, dan moeten ze als gelijk gelden. Omdat PHP de gelijkheidsoperator niet laat overladen, moet je dat zelf implementeren.

Je kunt daarvoor een aparte methode schrijven:

php
final class Price
{
    // ...

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

Zelfvalidatie

De validatie van een value object hoort bij het aanmaken te gebeuren. Is een van de eigenschappen ongeldig, dan gooi je een exception. Tel je daar de onveranderlijkheid bij op, dan weet je zeker dat een eenmaal aangemaakt value object altijd geldig blijft.

Terug naar het type Prijs: een negatief bedrag slaat binnen het domein van de applicatie nergens op.

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;
    }
}

Conclusie

Value objects helpen je schone code te schrijven. In plaats van dit:

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

schrijf je dit:

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

Dat leest makkelijker en redeneert makkelijker, en je hoeft niet meer uit te zoeken welk telefoonformaat je moet gebruiken. Omdat hun attributen ze definiëren en je ze met verschillende entiteiten kunt delen, kun je ze eeuwig cachen.

Ze helpen je ook duplicatie terug te dringen. In plaats van meerdere velden voor bedrag en valuta gebruik je één prijsklasse.

Zoals met alles in het leven kun je het uiteraard ook overdrijven met value objects in PHP. Stel je voor dat je bergen value objects omzet om ze in de database op te slaan, en ze bij het uitlezen weer terug omzet: dan koop je prestatieproblemen. Bovendien maakt een codebase vol value objects je project zwaarder.

Gebruik ze voor een veld of een groep velden uit je domein die validatie nodig hebben, of om dubbelzinnigheid weg te nemen, zoals bij telefoonformaten.

PHP

Damien Flandrin Webdeveloper sinds 2010, maker van Gekkode en Email Impact. Elk artikel wordt vóór publicatie getest op een echt project. Contact
Nieuwsbrief

Nieuwe tests, tutorials en projecten, per e-mail.

Reproduceerbare tests, geversioneerde code, gedateerde resultaten. Nooit spam.