POO en PHP : les enums peuvent implémenter des interfaces

Utilisation des enums en implémentant une interface pour bien les structurer.

→ Challenge Correction: Le Pôle Express

Ce corrigé est la « refactorisation POO » du premier corrigé du Pôle Express, qui permettait de découvrir l’utilisation de la structure match. match étant particulièrement utilisé dans les enums, je propose donc d’utiliser cette structure pour résoudre le challenge avec un code PHP orienté objet beaucoup plus structuré

Et j’en profite pour créer une interface et faire en sorte que mes enums implémentent cette interface pour bien les structurer.

Au programme :

Création de l’interface en PHP

Dans le challenge, 3 éléments font varier la température : le type de chocolat, l’épice, et un évènement particulier. Je vais donc créer une interface pour préciser qu’il y aura toujours une méthode qui fait varier la température. Cette méthode prend en paramètre une température initiale, et renvoie une température modifiée :

interface TemperatureModifier {
    public function modifyTemperature(int $temperature): int;
}

Création des 3 enums en PHP

Voici l’enum pour les types de chocolat :

enum ChocolateType: string implements TemperatureModifier
{
    case NOIR = 'noir';
    case AU_LAIT = 'au_lait';
    case BLANC = 'blanc';
    case MELANGE = 'melange';

    public function modifyTemperature(int $temperature): int
    {
        return match($this) {
            self::NOIR => $temperature + 5,
            self::AU_LAIT => $temperature + 10,
            self::BLANC => $temperature + 15,
            self::MELANGE => $temperature + 12,
        };
    }
}

Un peu d’explications :

  • L’enum est typé « string » car toutes ses valeurs sont représentées par des chaînes de caractères
  • L’enum implémente l’interface « TemperatureModifier ». La syntaxe est la même que pour une classe « classique »
  • J’écris mes différents cas en « upper case », comme je le ferais pour des constantes
  • J’utilise ma structure match pour gérer les différents cas dans la méthode modifyTemperature. Méthode que je suis obligé d’implémenter pour bien respecter l’interface. Sinon mon code (et même mon IDE) renverra une erreur.

Les 2 autres enums ont des structures similaires et peuvent être retrouvés sur Github.

La classe Order

Je décide de structurer chaque commande dans une classe « Order » :

class Order
{
    private function __construct(
        private int $temperature,
        public ChocolateType $chocolateType,
        public SpiceType $spiceType,
        public ?Event $event,
    ) { }


    public static function createFromText(string $informations): Order 
    {
        $data = explode(',', $informations);

        $temperature = (int) $data[0];
        $chocolateType = ChocolateType::from($data[1]);
        $spiceType = SpiceType::from($data[2]);
        $event = isset($data[3]) ? Event::from($data[3]) : null;

        return new self($temperature, $chocolateType, $spiceType, $event);
    }

    public function modifyTemperature(): void
    {
        $this->temperature = $this->chocolateType->modifyTemperature($this->temperature);
        $this->temperature = $this->spiceType->modifyTemperature($this->temperature);
        
        if (! is_null($this->event)) {
            $this->temperature = $this->event->modifyTemperature($this->temperature);
        }
    }

    public function getTemperature(): int
    {
        return $this->temperature;
    }
}

Un peu d’explications :

  • Je commence par faire de la promotion de propriétés dans le constructeur pour gérer mes différentes propriétés. La température est un entier mais les autres propriétés sont typées par les enums directement. Il se peut que je n’ai pas d’Event, c’est pourquoi j’ai le ? qui indique que la valeur peut être nulle.
  • J’ai ensuite une méthode statique createFromText. Cette méthode est chargée du parsing et de retourner une instance d’Order avec les bonnes propriétés. C’est plus intéressant de procéder de cette façon que de réaliser le parsing dans le constructeur.
  • La méthode « modifyTemperature » va appeler successivement les méthodes « modifyTemperature » de chaque enum, sauf l’Event si celui-ci est null.
  • Et un petit getteur pour récupérer la température car la propriété est privée.

Programme principal

Et voici le programme principal, forcément beaucoup + concis que dans la première version du corrigé !

$total = 0;

foreach ($orders as $orderInformations) {
    $order = Order::createFromText($orderInformations);
    $order->modifyTemperature();
    $total += $order->getTemperature();
}

$average = ceil($total / count($orders));

Un peu d’explications :

  • Je démarre $total à 0
  • Je boucle sur $orders
  • Je crée une $order en réalisant le parsing grâce à la méthode statique
  • Je modifie la température
  • J’incrémente $total
  • Et après la boucle, je calcule la moyenne, arrondie à l’entier supérieur !

Le code complet est disponible sur Github.

Retrouve aussi un autre corrigé qui utilise les enums en PHP.

Bonus : les tests unitaires avec Pest PHP pour bien verrouiller les possibles évolutions de son code

On va créer un fichier pour tester l’enum ChocolateType, qui est présent + haut dans le corrigé. Je vais donc vérifier que chaque cas est bien respecté :

test('La température augmente de 5 pour le chocolat noir', function () {
    $temperature = ChocolateType::NOIR->modifyTemperature(0);
    expect($temperature)->toBe(5);
});

test('La température augmente de 10 pour le chocolat au lait', function () {
    $temperature = ChocolateType::AU_LAIT->modifyTemperature(0);
    expect($temperature)->toBe(10);
});

test('La température augmente de 15 pour le chocolat blanc', function () {
    $temperature = ChocolateType::BLANC->modifyTemperature(0);
    expect($temperature)->toBe(15);
});

test('La température augmente de 12 pour le mélange de chocolat', function () {
    $temperature = ChocolateType::MELANGE->modifyTemperature(0);
    expect($temperature)->toBe(12);
});

Avec ces tests, je m’assure donc que chaque cas est géré correctement. Mais si je rajoute un cas dans mon enum, sans le gérer dans modifyTemperature, ni mon code, ni mon IDE, et ni mes tests ne m’indiqueront aucune erreur. Et c’est exactement ce que je cherche à faire, à ce que mes tests me préviennent de mon oubli. Voici une approche :

test('Tous les cas sont bien paramétrés dans modifyTemperature', function () {
    $initialTemperature = 10; // Une température initiale arbitraire
    foreach (ChocolateType::cases() as $chocolateType) {
        $chocolateType->modifyTemperature($initialTemperature);
    }


    $this->assertTrue(true);
});

Un peu d’explications :

  • La structure enums dispose de la méthode cases() qui permet de parcourir tous les cas d’un enum.
  • Pour chaque cas de mon enum, je lance donc la méthode modifyTemperature sur une température arbitraire.
  • On l’a vu dans l’article sur match, si un cas n’est pas géré, match lève une exception, mon test échouera et une erreur sera spécifiée, et le assertTrue(true) ne sera pas exécuté.
  • Si je n’ai pas rencontré d’erreur dans la boucle (= aucune exception levée), cela signifie que tous les cas sont bien gérés, je peux donc valider mon test avec assertTrue(true).
  • Quand mes tests me renvoient cette erreur, je comprends tout de suite que j’ai oublié d’ajouter la gestion du nouveau cas dans la méthode modifyTemperature !

On se rapproche ici de la notion de TDD : Test Driven Development. Ce sont mes tests en erreur qui vont m’indiquer quelles sont les portions de code que je dois encore travailler ! Pratique !


Qui a codé ce superbe contenu ?

Keep learning

Other content to discover