Un try/catch avec une exception personnalisée, comment ça marche ?

Utilisation d’un try/catch pour déceler l’arrêt du programme, avec une exception personnalisée.

→ Challenge Correction: Survie sur une île déserte #2

Comme le challenge SURVIVAL_2 présente une condition d’arrêt (cf. dans l’énoncé « L’exploration s’arrête ») qui peut survenir presque à n’importe quel moment de l’exécution du programme, je vais utiliser un try/catch avec une exception personnalisée. Avant cela, je vais structurer mon code dans une classe en et tester mon code avec Pest PHP.

Voici les différentes étapes :

  • POO : premier objet, constructeur, constantes et méthodes. Le code sera découpé en plein de petites méthodes.
  • Tests avec Pest PHP : ce découpage facilitera la création des tests.
  • Dataprovider : on verra comment écrire un dataprovider avec Pest PHP pour réaliser plusieurs tests d’un coup.
  • Exception personnalisée : explication et déclaration de cette structure, utile dans la gestion des erreurs ou des comportements anormaux de notre code.
  • Try/catch : structure liée grandement aux exceptions qui permet d’avoir un déroulé du programme en fonction de la présence, ou non, d’une exception.

Pour faciliter la lecture de l’article, le code complet peut être consulté sur Github.

POO en PHP : Constructeur, constantes et premières méthodes

Je crée une classe Survivor, et je définis des constantes pour les informations spécifiques de l’énoncé, à savoir les lettres qui représentent les différentes zones :

<?php
declare(strict_types=1);

namespace Challenges\SURVIVAL_2;

class Survivor
{
    public const ZONE_WATER = 'W';
    public const ZONE_FOOD = 'F';
    public const ZONE_DIFFICULT_FIELD = '_';

    public function __construct(
        public int $thirst,
        public int $hunger,
        public int $shape
    ) {}
}

J’utilise la promotion de propriétés dans le constructeur pour les 3 informations importantes du challenge : la soif, la faim et la forme de mon survivant.

Pour chacune des zones, je crée une méthode dédiée :

public function zoneWater(): void
{
    $this->thirst++;
    $this->shape--;
}

public function zoneFood(): void
{
    $this->hunger++;
    $this->shape--;
}

public function zoneDifficultField(): void
{
    $this->shape -= 3;
}

public function zoneSimple(): void
{
    $this->shape--;
}

Tests associés avec Pest PHP

Chacune de ces méthodes aura donc son test associé, réalisé ici avec Pest PHP, dans un fichier SurvivorTest.php :

test('Zone Water', function() {
    
    $survivor = new Survivor(
        thirst: 10,
        hunger: 10,
        shape: 100
    );

    $survivor->zoneWater();

    $this->assertEquals(11, $survivor->thirst);
    $this->assertEquals(99, $survivor->shape);
});

test('Zone Food', function() {
    $survivor = new Survivor(
        thirst: 10,
        hunger: 10,
        shape: 100
    );

    $survivor->zoneFood();

    $this->assertEquals(11, $survivor->hunger);
    $this->assertEquals(99, $survivor->shape);
});

test('Zone Terrain Difficile', function() {
    $survivor = new Survivor(
        thirst: 10,
        hunger: 10,
        shape: 100
    );

    $survivor->zoneDifficultField();

    $this->assertEquals(97, $survivor->shape);
});

test('Zone simple', function() {
    $survivor = new Survivor(
        thirst: 10,
        hunger: 10,
        shape: 100
    );

    $survivor->zoneSimple();

    $this->assertEquals(99, $survivor->shape);
});

Pour chaque test, on crée un survivant puis on applique une des méthodes. On s’assure alors que la ou les propriétés ont bien évoluées comme souhaité.

Condition d’arrêt et dataprovider

Le challenge doit s’arrêter si une des 3 propriétés tombe à zéro (ou moins). J’écris une méthode qui vérifie si on doit s’arrêter ou non :

public function mustStop(): bool
{
    return ($this->thirst <= 0 || $this->hunger <= 0 || $this->shape <= 0);
}

Pour retourner false, il faut que toutes les valeurs soient supérieures à zéro. Je peux écrire 1 seul test pour m’assurer de ce fonctionnement :

test('Peut continuer', function() {
    $survivor = new Survivor(
        thirst: 10,
        hunger: 10,
        shape: 100
    );

    $this->assertFalse($survivor->mustStop());
});

Par contre, il y a plusieurs combinaisons de valeurs possibles qui vont retourner true. Je vais utiliser un dataprovider avec la méthode « with » :

test('Doit s\'arrêter', function(int $thirst, int $hunger, int $shape) {
    $survivor = new Survivor(
        thirst: $thirst,
        hunger: $hunger,
        shape: $shape
    );

    $this->assertTrue($survivor->mustStop());
})->with([
    'Soif à zéro' => [0, 10, 100],
    'Faim à zéro' => [10, 0, 100],
    'Forme à zéro' => [10, 10, 0],
]);

Exception personnalisée en PHP

Je crée d’abord une méthode qui permet de passer en revue une zone. A l’aide d’un switch, je vais me rediriger dans telle ou telle méthode :

public function zone(string $zone): void
{
    switch ($zone) {
        case self::ZONE_WATER:
            $this->zoneWater();
        break;

        case self::ZONE_FOOD:
            $this->zoneFood();
        break;
        
        case self::ZONE_DIFFICULT_FIELD:
            $this->zoneDifficultField();
        break;

        default:
            $this->zoneSimple();
        break;
    }
}

Et la méthode region qui permet de parcourir toute une région, c’est à dire une chaine de caractères qui comporte plusieurs zones à explorer :

public function region(string $region): void
{
    foreach (str_split($region) as $zone) {
        $this->zone($zone);
        if ($this->mustStop()) {
            throw new EndOfAdventureException;
        }
    }
}

Quelques explications :

  • Le str_split me permet de transformer la chaine de caractères en tableau pour utiliser un foreach plutot qu’un for
  • « Si je dois m’arrêter » (if mustStop) alors j’envoie une Exception personnalisée qui signifie la fin de mon aventure.

Voici comment je crée cette exception personnalisée :

<?php
declare(strict_types=1);

namespace Challenges\SURVIVAL_2;

class EndOfAdventureException extends \Exception
{
    public $message = 'End of Adventure';
}

Créer ses propres exceptions permet de clarifier la lecture du code et d’organiser correctement la gestion des erreurs dans son programme, dans son application.

Une exception peut être testée avec la méthode « expectException ». Il faut d’abord écrire la ligne avec « exceptException » pour indiquer qu’on « s’attend à avoir une exception », puis la ligne qui la déclenche :

test('Région difficile', function() {
    $survivor = new Survivor(
        thirst: 10,
        hunger: 10,
        shape: 10
    );

    $this->expectException(EndOfAdventureException::class);
    $survivor->region('_________'); // équivalent de -30
});

Try catch en PHP

Voici la méthode night() qui réalise la mise à jour des propriétés durant la nuit :

public function night(): void
{
    $this->shape += (int) floor(($this->thirst + $this->hunger) / 2);

    $this->thirst -= 5;
    $this->hunger -= 5;
}

Et je crée une méthode adventure qui prend en paramètre une ile, c’est à dire un ensemble de régions (donc un tableau de chaines de caractères) :

/**
 * @param string[] $island
 */
public function adventure(array $island): void
{
    foreach ($island as $region) {

        try {
            $this->region($region);
        } catch (EndOfAdventureException $e) {
            echo $e->message;
            return;
        }

        $this->night();
        if ($this->mustStop()) {
            return;
        }
    }
}

Quelques explications :

  • Bien que typé « void », je peux utiliser return; pour terminer la méthode.
  • Le try/catch va me permettre de détecter l’erreur de fin d’aventure. Cette exception est donc émise si après l’exploration d’une zone, une des caractéristiques est passé à zéro. Le code compris dans le catch sera exécuté si l’exception recherchée est bien celle émise. Par contre, si $this->region() n’émet pas d’exception, le code compris dans catch ne sera pas exécuté.
  • Pour $this->night(), on aurait pu utiliser également un try catch mais le code est un peu différent car toutes les actions de night() sont exécutées, quoi qu’il arrive. Le contrôle peut donc se faire après.

Et pour finir le challenge ?

J’ai décrit dans cet articles les éléments intéressants du corrigé. Tout le reste du code peut être trouvé sur Github avec notamment :

  • Le test de night()
  • Un autre test de region()
  • Le calcul du résultat et son test associé (avec un dataprovider)
  • Un test avec un jeu de données complet

J’ai utilisé Pest PHP dans ce corrigé. Si tu le souhaites tu peux retrouver notre introduction à Pest PHP.


Qui a codé ce superbe contenu ?

Keep learning

Other content to discover