Newsletter

Value Object in PHP: immutabilità, uguaglianza e validazione

Value Object in PHP: immutabilità, uguaglianza e validazione

Un value object in PHP è un tipo che racchiude dei dati e che si distingue soltanto per le sue proprietà. A differenza di un’entità, non possiede un identificatore univoco. Due value object in PHP con gli stessi valori di proprietà vanno quindi considerati uguali.

Buoni candidati a diventare value object sono:

  • prezzo
  • numero di telefono
  • indirizzo
  • un hash di commit
  • un identificatore di entità
  • e così via.

Quando progetti un value object in PHP devi curare le sue tre caratteristiche principali: immutabilità, uguaglianza strutturale e auto-validazione.

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

Immutabilità

Una volta istanziato, un value object in PHP deve restare identico per tutto il resto del ciclo di vita dell’applicazione. Se devi modificarne il valore, devi sostituire l’oggetto per intero.

Usare un value object mutabile è accettabile finché resta confinato in un ambito locale, con un solo riferimento all’oggetto. Altrimenti rischi di incontrare qualche problema.

Riprendendo l’esempio precedente, ecco come aggiornare l’importo di un tipo Price:

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

Uguaglianza strutturale

I value object in PHP non hanno identificatore. In altre parole, se due value object hanno gli stessi valori interni, vanno considerati uguali. Siccome PHP non permette di ridefinire l’operatore di uguaglianza, devi implementarla tu.

Puoi creare un metodo dedicato:

php
final class Price
{
    // ...

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

Auto-validazione

La validazione di un value object deve avvenire alla sua creazione. Se una delle sue proprietà non è valida, va sollevata un’eccezione. Se a questo aggiungi l’immutabilità, una volta creato un value object puoi essere sicuro che resterà valido per sempre.

Riprendendo l’esempio del tipo Price, un importo negativo non ha senso nel dominio dell’applicazione:

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

Conclusione

I value object aiutano a scrivere codice pulito. Invece di scrivere:

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

puoi scrivere:

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

Si legge e si ragiona meglio, e non devi più chiederti quale formato di telefono usare. Dato che sono definiti dai loro attributi e che puoi condividerli con entità diverse, possono restare in cache per sempre.

Aiutano anche a ridurre le duplicazioni. Invece di avere più campi importo e valuta, puoi usare un’unica classe Price.

Naturalmente, come tutto nella vita, i value object in PHP non vanno abusati. Immagina di convertire tonnellate di value object per salvarli nel database, o di riconvertirli in value object quando li rileggi: rischi problemi di prestazioni. In più, avere tonnellate di value object può appesantire la tua base di codice.

Usali per rappresentare un campo o un gruppo di campi del tuo dominio che richiedono una validazione, o per togliere ogni ambiguità, come nel caso dei formati di telefono.

PHP

Damien Flandrin Sviluppatore web dal 2010, creatore di Gekkode e di Email Impact. Ogni articolo è testato su un progetto reale prima della pubblicazione. Contatti
Newsletter

I nuovi test, tutorial e progetti, via e-mail.

Test riproducibili, codice versionato, risultati datati. Mai spam.