# « Uncaught ValueError » sur un enum de rôle après une extension tierce mal mise à jour

> Un enum PHP qui modélise les rôles se met soudain à planter après la mise à jour d'une extension. Le diagnostic pointe une valeur non prévue, injectée depuis l'extérieur.

- Auteur : Clément Hadrot
- Publié le : 2024-12-11
- Mis à jour le : 2024-12-11
- Catégorie : Sécurité
- URL : https://wpmoderne.dev.wordpress-developpement.fr/securite/valueerror-enum-role-extension-tierce/

## L’essentiel

- Un enum PHP échoue strictement si la valeur ne correspond à aucun cas
- Une extension tierce peut introduire un rôle inattendu sans le signaler
- Prévoir un cas de repli explicite plutôt que de faire confiance à la source

```
Fatal error: Uncaught ValueError: "redacteur_partenaire" is not a valid backing value for enum RoleUtilisateur
```

Ce message est apparu sur un projet qui modélisait les rôles utilisateurs via un enum PHP à valeurs (`enum RoleUtilisateur: string`), quelques minutes après la mise à jour automatique d'une extension de gestion de contenu partenaire. Le code de l'enum n'avait pas changé ; c'est la donnée reçue en entrée qui, soudainement, ne correspondait plus à aucun des cas déclarés.

## Symptôme : le fatal apparaît sans modification du code

L'enum en question ressemblait à ceci :

```
enum RoleUtilisateur: string {
    case Administrateur = 'administrateur';
    case Redacteur      = 'redacteur';
    case Contributeur   = 'contributeur';

    public static function depuisRoleWordPress( string $role ): self {
        return self::from( $role );
    }
}
```

La méthode statique `from()`, native aux enums PHP à valeurs depuis PHP 8.1, lève strictement un `ValueError` si la chaîne fournie ne correspond à aucun cas déclaré — contrairement à un tableau associatif classique, qui aurait simplement renvoyé `null` sur une clé absente. C'est précisément ce comportement strict, généralement recherché pour éviter les erreurs silencieuses, qui a provoqué le fatal ici.

## Diagnostic : une extension a introduit un rôle non prévu

> L'essentiel à retenir : Un enum PHP échoue strictement si la valeur ne correspond à aucun cas ; Une extension tierce peut introduire un rôle inattendu sans le signaler ; Prévoir un cas de repli explicite plutôt que de faire confiance à la source

L'investigation a montré que la mise à jour de l'extension partenaire avait ajouté, via `add_role()`, un nouveau rôle WordPress nommé `redacteur_partenaire`, destiné à un usage interne à cette extension. Le code applicatif, qui appelait `RoleUtilisateur::depuisRoleWordPress( $wp_user->roles[0] )` pour chaque utilisateur affiché dans un tableau de bord personnalisé, s'est retrouvé à recevoir cette nouvelle valeur dès qu'un utilisateur test s'est vu attribuer ce rôle par l'extension.

Le problème ne venait donc ni d'une régression du code applicatif, ni d'un bug de l'extension à proprement parler : l'extension avait le droit d'ajouter un rôle, et le code applicatif n'avait simplement jamais anticipé qu'un rôle inconnu de son enum puisse un jour se présenter en entrée.

## Correctif : un cas de repli explicite

La correction consiste à ne jamais utiliser `from()` directement sur une valeur dont l'origine n'est pas entièrement maîtrisée, et à privilégier `tryFrom()`, qui renvoie `null` plutôt que de lever une exception en cas de valeur inconnue :

```
enum RoleUtilisateur: string {
    case Administrateur = 'administrateur';
    case Redacteur      = 'redacteur';
    case Contributeur   = 'contributeur';
    case Inconnu        = 'inconnu';

    public static function depuisRoleWordPress( string $role ): self {
        return self::tryFrom( $role ) ?? self::Inconnu;
    }
}
```

Ajouter un cas explicite `Inconnu` plutôt que de renvoyer `null` directement permet de conserver un typage strict tout au long du code applicatif : toute logique qui consomme `RoleUtilisateur` continue de travailler avec une instance de l'enum, sans avoir à gérer un cas `null` supplémentaire à chaque appel.

## Prévention : ne jamais faire confiance à une source externe pour un enum fermé

- Utiliser `tryFrom()` systématiquement dès que la valeur provient d'une donnée externe au module qui définit l'enum
- Réserver `from()` aux cas où la valeur est produite par le même module, avec une garantie de cohérence à la compilation
- Journaliser les valeurs inconnues rencontrées, pour détecter rapidement l'apparition d'un nouveau cas non prévu

```
public static function depuisRoleWordPress( string $role ): self {
    $resultat = self::tryFrom( $role );
    if ( null === $resultat ) {
        error_log( sprintf( 'Rôle WordPress non modélisé rencontré : %s', $role ) );
        return self::Inconnu;
    }
    return $resultat;
}
```

## Une leçon plus large sur les enums fermés

Ce cas illustre une tension propre aux enums PHP à valeurs : leur intérêt principal réside dans leur caractère fermé et exhaustif, ce qui les rend particulièrement adaptés à un ensemble de valeurs entièrement maîtrisé par le même code. Dès qu'une valeur peut provenir d'une source externe — une extension tierce, une configuration modifiable, une API — cette fermeture devient un risque de rupture si elle n'est pas explicitement gérée avec un cas de repli.

> Un enum qui modélise une donnée externe doit toujours prévoir le cas d'une valeur qu'il ne connaît pas encore.

## En résumé

Le fatal observé ici ne venait ni d'un bug du cœur WordPress, ni d'une erreur de l'extension partenaire, mais d'une hypothèse implicite du code applicatif : que la liste des rôles resterait figée. Remplacer `from()` par `tryFrom()` avec un cas de repli explicite, dès qu'un enum modélise une donnée dont la source peut évoluer indépendamment du code qui le consomme, évite ce type de rupture en production.
