Les Types Union et l’Expression match de PHP 8

Pourquoi ces nouveautés sont importantes ?

Depuis sa première version, PHP a régulièrement évolué pour offrir aux développeurs davantage de productivité, une meilleure robustesse et une lisibilité accrue de leurs applications. Avec PHP 8, deux ajouts majeurs changent particulièrement la donne :

  • Les Types Union (|) : Cette fonctionnalité vous permet d’indiquer clairement qu’une variable, un paramètre ou une fonction peut manipuler plusieurs types. Plus besoin de docblocks ambigus : le moteur PHP valide désormais que la valeur renvoyée correspond à un des types autorisés (ex. int|string, User|null, etc.).
  • L’expression match : Successeur modernisé du switch, match permet d’écrire des branches de logique plus concises et plus robustes grâce à une comparaison stricte (équivalent de ===), l’absence de fall-through involontaire et la possibilité de retourner directement une valeur.

Dans cet article, je vais découvrir comment et pourquoi tirer parti de ces nouveautés dans vos projets PHP 8, que ce soit pour renforcer la sécurité de type, améliorer la lisibilité de vos conditions ou remplacer des switch longs et répétitifs.

1. Les Types Union en PHP 8

1.1 Qu’est-ce qu’un type union ?

Avant PHP 8, lorsqu’une fonction pouvait retourner différents types, il n’existait pas de moyen natif de l’indiquer clairement. On se contentait bien souvent de docblocks ou de retours non typés. Désormais, grâce aux Types Union, vous pouvez spécifier plusieurs types possibles pour vos paramètres ou vos retours de fonction.


function parseNumber(string $input): int|float {
    if (str_contains($input, '.')) {
        return (float) $input;
    }

    return (int) $input;
}
    

Dans l’exemple ci-dessus, la fonction parseNumber peut retourner soit un int, soit un float, en fonction de la présence ou non d’un point décimal dans la chaîne.

1.2 Pourquoi utiliser les types union ?

  • Lisibilité du code : Les Types Union rendent explicite les différents types gérés, ce qui facilite la compréhension et la maintenance du code.
  • Sécurité de type : Le moteur PHP valide que la valeur renvoyée correspond à l’un des types autorisés, limitant les comportements imprévus.
  • API plus claire : Lorsqu’on publie une bibliothèque ou lorsqu’on travaille en équipe, spécifier clairement les types attendus et retournés évite les mauvaises utilisations et facilite la documentation.

1.3 Combiner plusieurs types (objets, null, erreurs, etc.)

Il arrive qu’une fonction puisse retourner, selon le contexte, soit un objet précis, soit un objet représentant une erreur, ou encore null. PHP 8 rend cela très facile :


 1000) {
        return null;
    }

    // Sinon, on retourne un User
    return new User($id, "John Doe");
}

// Utilisation
$result = getUser(42);

if ($result instanceof User) {
    echo "Utilisateur trouvé : " . $result->name;
} elseif ($result instanceof ErrorObject) {
    echo "Erreur : " . $result->message;
} else {
    echo "Aucun utilisateur trouvé";
}
    

Dans ce scénario, la fonction getUser peut renvoyer trois types : User, ErrorObject ou null. Grâce aux Types Union, la signature de la fonction le rend parfaitement explicite.

2. L’Expression match en PHP 8

2.1 Présentation

L’expression match est une alternative moderne au traditionnel switch. Contrairement à switch :

  • La comparaison est stricte : équivalent de ===, au lieu de ==.
  • Elle retourne une valeur : plus besoin de stocker dans une variable manuellement à chaque branche.
  • Pas de risque de "fall-through" : les branches n’ont plus besoin de se terminer par un break.

2.2 Syntaxe de base

Voici un exemple de base illustrant la différence entre switch et match.


// Ancien code avec switch
$color = 'red';
switch ($color) {
    case 'red':
        $hex = '#FF0000';
        break;
    case 'green':
        $hex = '#008000';
        break;
    case 'blue':
        $hex = '#0000FF';
        break;
    default:
        $hex = '#000000';
}
    

// Nouveau code avec match
$color = 'red';
$hex = match ($color) {
    'red'   => '#FF0000',
    'green' => '#008000',
    'blue'  => '#0000FF',
    default => '#000000',
};
    

Avec match, plus besoin de break. Chaque branche retourne directement la valeur souhaitée.

2.3 Combiner match et appels de fonctions

Il est tout à fait possible d’exécuter du code plus complexe ou d’appeler des fonctions au sein des branches :


function handleColor(string $color): string {
    return match ($color) {
        'red'   => strtoupper('#ff0000'),
        'green' => strtoupper('#008000'),
        'blue'  => strtoupper('#0000ff'),
        default => defaultColorValue(),
    };
}

function defaultColorValue(): string {
    // Imaginons un mécanisme plus complexe ici
    return '#000000';
}

echo handleColor('red');    // #FF0000
echo handleColor('purple'); // #000000 (valeur par défaut)
    

2.4 Gérer des conditions plus complexes

Bien que match compare avant tout des valeurs par égalité stricte, on peut l’utiliser avec des conditions plus élaborées. L’astuce consiste à faire le match sur la valeur true, puis à placer la logique dans chaque branche :


function matchComplex(int $number): string {
    return match (true) {
        $number < 0    => "Nombre négatif",
        $number === 0  => "Zéro",
        $number > 100  => "Très grand nombre",
        default        => "Nombre positif",
    };
}

echo matchComplex(-5);  // Nombre négatif
echo matchComplex(0);   // Zéro
echo matchComplex(150); // Très grand nombre
echo matchComplex(42);  // Nombre positif
    

Ce pattern est parfois controversé (car il détourne un peu l’idée initiale de match), mais il demeure pratique lorsqu’on souhaite gérer des branches conditionnelles plus complexes tout en bénéficiant de la syntaxe plus concise de match.

3. Bonnes pratiques et scénarios d’utilisation

  1. Types union et null : Évitez d’abuser de null comme valeur de retour. Si un échec est fréquent, vous pourriez préférer lancer des exceptions ou renvoyer un objet dédié. Cependant, indiquer qu’une fonction peut retourner null dans un type union reste pertinent dans certains cas.
  2. Lisibilité du match : Pour maintenir un code propre, évitez de mettre trop de logique dans chaque branche. Si la structure devient complexe, extrayez la logique dans des fonctions dédiées.
  3. Refactorisation : Dans le cas de projets legacy, il est possible de migrer progressivement les switch simples vers match. Veillez à vérifier les cas où un fall-through volontaire aurait pu être utilisé par le passé.
  4. Documentation : Avec les Types Union, mettez à jour vos docblocks et la documentation du projet (README, wiki, etc.) pour signaler clairement les retours possibles.
  5. Combiner match et retours type union : Il est possible de renvoyer différents types selon les branches d’un match :
    
    function processInput(int $value): string|int {
        return match (true) {
            $value < 10   => "Trop petit",
            $value === 10 => $value,        // Renvoie un int
            default       => "Valeur OK",   // Renvoie un string
        };
    }
                

4. Exemple complet : Types Union + match

Pour terminer, voici un exemple concret mêlant Types Union et match dans un contexte e-commerce où différents statuts de commande sont traités :


 new Command($orderId, 99.99),
        'cancelled' => new CancelledCommand($orderId, "Annulation demandée par le client."),
        'error'     => new ErrorObject("Erreur interne lors du traitement de la commande."),
        default     => new ErrorObject("Status inconnu: " . $status),
    };
}

// Exemple d'utilisation
$result = processOrderStatus(12345, 'cancelled');

if ($result instanceof Command) {
    echo "Commande " . $result->orderId . " traitée. Montant: " . $result->amount;
} elseif ($result instanceof CancelledCommand) {
    echo "Commande " . $result->orderId . " annulée. Raison: " . $result->reason;
} else {
    // Ici, $result est forcément un ErrorObject
    echo "Erreur: " . $result->message;
}
    

Cet exemple montre qu’en combinant Types Union et l’expression match, il est possible de rendre la logique de traitement de différents statuts de commande claire et explicite.