POO en PHP : exemples de classes, tests unitaires et fonctions intéressantes

Un bon exercice pour mettre en pratique les principes de la POO en PHP avec quelques fonctions intéressantes comme les variable variable.

→ Challenge Correction: Jeux Olympiques – Tableau des médailles

Un challenge Tainix est toujours l’occasion de mettre en pratique certains concepts intéressants de la programmation. Dans ce corrigé, on va utiliser des classes pour réaliser un challenge débutant. Mais ce n’est pas tout, on va aussi voir au passage quelques fonctionnalités intéressantes et réaliser des tests unitaires.

Au programme :

Les fonctions et concepts intéressants qui seront passés en revue : PHPDoc, parsing, promotion de propriétés, « variable variale », tri sur mesure, etc.

Une classe PHP pour parser les données

Pour résoudre ce challenge, il faut commencer par parser les données d’entrée. Elles sont sous ce format :

BEL : Bronze,Bronze,Silver,Gold,Bronze

  • « BEL » est le code Nation, je veux le récupérer tel quel
  • « Bronze,Bronze,Silver,Gold,Bronze » sont les médailles pour ce pays, je veux les récupérer dans un tableau pour les parcourir facilement par la suite

Je vais mettre le code qui réalise ce parsing dans une classe dédiée, avec une méthode statique. Comme ça, pas besoin d’instancier la classe pour appeler la méthode :

class Parser
{
    /**
     * @return array{code: string, medals: string[]}
     */
    public static function parse(string $informations): array
    {
        $data = explode(':', $informations);
        
        return [
            'code' => trim($data[0]),
            'medals' => explode(',', trim($data[1]))
        ];
    }
}

Un peu d’explications :

  • La méthode prend en paramètre une chaine de caractères et renvoie un tableau
  • La PHPDoc respecte les conventions de PHPStan pour décrire le tableau de sortie
  • Je réalise un explode sur « : » pour séparer correctement les informations. Je ne me soucie pas des espaces, c’est la fonction trim qui se chargera de les retirer.
  • Pour le retour, pour « medals », j’utilise à nouveau explode, cette fois-ci sur « , » pour séparer les médailles entre elles.

Je pourrais utiliser cette méthode statique de cette façon :

$data = Parser::parse($informations);

Si tu n’es pas familier avec la fonction explode ou avec le parsing en général, tu peux retrouver notre article dédié au parsing avec PHP.

Une classe PHP pour organiser les données des Nations

Les Nations ont plusieurs informations :

  • Un code de 3 lettres
  • Des médailles, qu’on peut organiser en 3 compteurs distincts, un par type de médaille (Bronze, Argent et Or)
  • A la construction, le code doit être déclaré, et chaque compteur démarre à 0.

On peut commencer à coder cette classe de cette façon :

class Nation
{
    private int $nbGold = 0;
    private int $nbSilver = 0;
    private int $nbBronze = 0;
    
    public function __construct(
        public readonly string $code    
    ) {}
}

Un peu d’explications :

  • Mes 3 compteurs sont nommés avec la même structure « nb… » et tous initialisés à 0.
  • Dans mon constructeur, je fais de la promotion de propriétés en PHP. Cela me permet de déclarer et initialiser $code. La propriété est « public readonly », ce qui signifie que je peux la lire de l’extérieur de ma classe, mais je ne pourrais pas la modifier. $code ne peut être instancié qu’une seule fois et il est grandement conseillé de le faire dans le constructeur.

Ensuite, j’ai besoin d’une méthode pour ajouter une médaille. Si on reprend la structure des données, les médailles sont représentées par une chaine de caractères : « Bronze, « Silver » ou « Gold ». Ca tombe bien, ça ressemble beaucoup aux noms de mes propriétés…

Je vais pouvoir utiliser une « variable variable » :

public function addMedal(string $medal): void
{
    $var = 'nb' . $medal; // Utilisation d'une "variable variable"
    $this->$var++;
}

Cette syntaxe me permet de définir le nom de ma variable, puis d’appeler ma variable en utilisant son nom. C’est ce qu’on appelle une « variable variable« . Attention, cette technique est à utiliser dans des cas précis et maitrisés. Utiliser cette technique sans réfléchir peut créer des problèmes de sécurité, un peu comme l’utilisation de eval en PHP.

Enfin, j’ai besoin de 2 autres méthodes :

  • Une méthode qui calcule le score de la Nation, en fonction de ses médailles et de la valeur de chacune (10 pour l’or, 5 pour l’argent et 2 pour le bronze)
  • Une méthode qui va retourner l’attendu du challenge, à savoir une chaine de caractères composée du code et du score, séparés par un underscore

Voici le code :

public function getScoreTotal(): int
{
    return $this->nbGold * 10 + $this->nbSilver * 5 + $this->nbBronze * 2;
}

public function getCodeAndScore(): string
{
    return $this->code . '_' . $this->getScoreTotal();
}

On utilise ici les propriétés et la méthode de l’objet, en utilisant $this, soit pour faire des calculs, soit pour faire de la concaténation.

Une classe PHP pour organiser et trier les Nations

Je crée une dernière classe « Table », qui représente le tableau des médailles avec toutes les nations.

Cette classe va contenir :

  • Une propriétés $nations qui sera un tableau d’instances de la classe Nation qui permettra de stocker toutes les nations du challenge
  • Une méthode pour ajouter une Nation dans ce tableau
  • Enfin, une méthode, avec un tri personnalisé, qui retournera la « meilleure » nation, c’est à dire celle qui a le score le + élevé.

Voici le code :

class Table
{
    /**
     * @var Nation[] $nations
     */
    private array $nations = [];
    
    public function addNation(Nation $nation): void
    {
        $this->nations[] = $nation;
    }
    
    public function getBestNation(): Nation
    {
        usort($this->nations, function (Nation $n1, Nation $n2) {
            return $n2->getScoreTotal() <=> $n1->getScoreTotal(); 
        });
        
        return $this->nations[0];
    }
}

Un peu d’explications :

  • J’utilise le typage dans addNation pour bien préciser que j’attends une instance de la classe Nation en paramètre. Ce n’est pas Table qui sera chargé du parsing.
  • Dans la méthode getBestNation, j’utilise la fonction native PHP usort() qui permet d’appliquer un tri sur mesure à un tableau. Dans la fonction sur mesure utilisée, j’utilise la méthode getScoreTotal de l’objet Nation comme « condition » de tri. A la fin je retourne la première nation, c’est à dire celle indexée en position 0.
  • Avec le typage, je précise que la méthode getBestNation() doit retourner l’instance d’une Nation

Programme principal pour résoudre le challenge en PHP

Pour rappel, les données du challenge sont stockés dans une variable $table. Voici le code du programme principal :

$tableOfMedals = new Table;

foreach ($table as $informations) {

    $data = Parser::parse($informations);
    
    $nation = new Nation($data['code']);
    
    foreach ($data['medals'] as $medal) {
       $nation->addMedal($medal);
    }
    
    $tableOfMedals->addNation($nation);
}

$result = $tableOfMedals->getBestNation()->getCodeAndScore();

Un peu d’explications :

  • Je n’ai pas de constructeur pour la classe Table, il n’y a donc pas de paramètre lors du « new Table » de création du tableau des médailles.
  • Je boucle ensuite sur $table pour extraire les lignes d’informations une par une.
  • Je parse ces informations à l’aide la méthode parse de ma classe Parser et je stocke les données dans $data.
  • Je crée une nation à l’aide du « code » de $data.
  • Puis pour chaque « medals » contenues dans $data, je l’ajoute à ma nation via la méthode addMedal (celle qui fait appel à la notion de variable variable).
  • Quand j’ai ajouté toutes les médailles, je peux mettre la Nation dans le tableau des médailles via la méthode addNation
  • Pour extraire le résultat, je récupère la meilleure nation (le tri est alors effectué via usort). Comme le retour est une instance de Nation, je peux directement appliquer la méthode getCodeAndScore() de façon chainée pour récupérer la réponse de mon challenge !

Ce code pourrait être critiqué à quelques égards :

  • On aurait pu se passer des foreach dans le programme principal et réaliser les boucles directement dans les objets par exemple.
  • On pourrait créer des constantes pour les « valeurs » des médailles
  • On pourrait discuter de « qui » doit calculer le score d’une Nation pour bien respecter le principe de Single Responsability ? Est ce Nation ou Table ou une autre classe dédiée ?

Les tests unitaires avec Pest PHP

On va tester pour ce code :

  • Que les éléments du parsing sont bien récupérés
  • Qu’on arrive à attribuer correctement chaque type de médaille
  • Que le calcul du score s’effectue correctement
  • Que la chaine de caractères de sortie d’une Nation est bien celle attendue
  • Que le tableau des médailles ordonnent correctement les Nations

Et voici les tests, avec la librairie de tests unitaires Pest PHP :

/**
 * Parser
 */
test('Parsing des données', function() {

    $informations = 'BEL : Bronze,Bronze,Silver,Gold,Bronze';

    $data = Parser::parse($informations);

    expect($data['code'])->toBe('BEL');
    expect($data['medals'])->toBe(['Bronze', 'Bronze', 'Silver', 'Gold', 'Bronze']);
});

/**
 * Nation
 */
test('Attribution d\'une médaille de bronze', function() {

    $nation = new Nation('JPN');
    $nation->addMedal('Bronze');

    expect($nation->getScoreTotal())->toBe(2);
});

test('Attribution d\'une médaille d\'argent', function() {

    $nation = new Nation('ITA');
    $nation->addMedal('Silver');

    expect($nation->getScoreTotal())->toBe(5);
});

test('Attribution d\'une médaille d\'or', function() {

    $nation = new Nation('NOR');
    $nation->addMedal('Gold');

    expect($nation->getScoreTotal())->toBe(10);
});

test('Attribution d\'une médaille de chaque', function() {

    $nation = new Nation('GER');
    $nation->addMedal('Bronze');
    $nation->addMedal('Silver');
    $nation->addMedal('Gold');

    expect($nation->getScoreTotal())->toBe(17); // 2 + 5 + 10
});

test('Affichage du code et score d\'une Nation', function() {

    $nation = new Nation('CAN');
    $nation->addMedal('Bronze');
    $nation->addMedal('Bronze');
    $nation->addMedal('Silver');
    $nation->addMedal('Gold');

    expect($nation->getCodeAndScore())->toBe('CAN_19'); // 2 + 2 + 5 + 10
});

/**
 * Table
 */
test('Sortie de la meilleure Nation', function() {

    $aus = new Nation('AUS');
    $aus->addMedal('Bronze');
    
    $swe = new Nation('SWE');
    $swe->addMedal('Silver');
    
    $jam = new Nation('JAM');
    $jam->addMedal('Gold');

    $table = new Table;
    $table->addNation($aus);
    $table->addNation($swe);
    $table->addNation($jam);

    expect($table->getBestNation())->toBe($jam);
});

N’hésite pas à choisir un autre challenge de code pour mettre en pratique la POO avec PHP !


Qui a codé ce superbe contenu ?

Keep learning

Other content to discover