Le pattern Optional, le conteneur de valeur qui va remplacer vos données nullables

Nous manipulons tous au quotidien des données nullables, or manipuler des données qui peuvent être null implique de devoir effectuer de nombreuses vérifications afin de savoir si la donnée que l’on manipule contient bien une valeur avant de l’utiliser. Auquel cas, nous aurons une erreur de type:

PHP Warning:  Uncaught Error: Call to a member function method() on null in php shell code:1
Stack trace:
#0 {main}
  thrown in php shell code on line 1

Pour éviter l’utilisation de conditions de type if ($var !== null), il est possible d’utiliser le pattern Optional qui peut s’apparenter à la monade Maybe.

Un Optional est un conteneur permettant d’encapsuler une valeur qui peut être présente ou non. Ainsi, plutôt que d’utiliser une variable null pour représenter une valeur absente, nous allons pouvoir manipuler systématiquement un objet et appeler des méthodes sur ce dernier qui effectuera des opérations en fonction que la valeur soit présente ou non.

Prenons un exemple concret pour illustrer ce concept. Sur un site d’e-commerce, un événement est émis une fois un achat effectué par un utilisateur afin de lui envoyer un récapitulatif de sa commande. La gestion de l’envoi de l’email pourrait ressembler au code suivant:

class Customer
{
    public ?string $email;
}

readonly class OrderConfirmed
{
    public Customer $customer;
}

readonly class OrderNotification
{
    public function __construct(private EmailSender $email) {}

    public function sendConfirmatinEmailOnOrderConfirmed(OrderConfirmed $event): void
    {
        if ($event->getCustomer() == null) { // invite account
            return; 
        }


        if ($event->getCustomer()->getEmail() == null) { // no email filled
            return; 
        }

        $this->email->send($event->getCustomer()->getEmail(), 'Email content');
    }
}

Nous voyons dans ce cas d’exemple, qu’il est nécessaire de faire deux vérifications avant de pouvoir envoyer notre e-mail de confirmation:

  1. Dans un premier temps, il est nécessaire de vérifier que la commande n’a pas été effectuée en “mode invité”, ce qui impliquerait d’avoir une commande sans notion d’acheteur,
  2. Il est ensuite nécessaire de vérifier que l’on a bien une adresse de renseignée pour pouvoir envoyer l’e-mail.

Voyons maintenant ce que pourrait donner ce même code avec l’utilisation du pattern Optional:

class Customer
{
    /** @var Optional<string> */
    public Optional $email;
}

readonly class OrderConfirmed
{
    /** @var Optional<Customer> */
    public Optional $customer;
}

readonly class OrderNotification
{
    public function __construct(private EmailSender $email) {}

    public function sendConfirmatinEmailOnOrderConfirmed(OrderConfirmed $event): void
    {
        $event->customer
            ->flatMap(static fn (Customer $customer) => $customer->email)
            ->ifPresent(static fn (string $email) => $this->email->send($event->getCustomer()->getEmail(), 'Email content'));
    }
}

Dans cette seconde version, nous constatons que toutes nos vérifications (les if) ont été supprimées. Le code s’en retrouve plus concis et surtout se concentre sur les opérations utiles au traitement de l’envoi de notre e-mail, ce qui le rend plus expressif. Il n’est plus question de devoir vérifier si la donnée est présente ou non, la structure de donnée Optional va appliquer les transformations demandées uniquement si la valeur est présente et ne fait rien dans le cas contraire.

Les monades gagnant en popularité ces dernières années, on commence à voir des implémentations du pattern Optional directement dans les langages de programmation. Ce n’est (malheureusement) pas le cas en PHP. Mais on retrouve quelques implémentations sur Packagist.

Bien que son utilisation dans l’écosystème PHP reste marginale, à la vue des nombreux avantages qu’il représente, vous avez tout intérêt à regarder ça de plus près. Pour ma part, j’ai commencé à introduire cette notion en construisant ma propre implémentation qui se résume à quelques lignes de code.