Comment représenter, dans la signature d’une fonction, le fait qu’un appel à un modèle de langage peut aboutir à deux résultats de nature différente ? Un texte généré d’un côté, un échec identifié de l’autre : quota dépassé, contenu filtré, réponse tronquée. Les union types de PHP 8.0 offrent une réponse simple, sans recourir systématiquement aux exceptions.
L’enjeu n’est pas anecdotique. Une fonction qui appelle un modèle de langage échoue régulièrement pour des raisons qui ne sont pas des bogues : un quota atteint fait partie du fonctionnement normal du système, pas d’un état exceptionnel au sens strict.
Le problème des exceptions omniprésentes
Lever une exception à chaque échec de l’appel oblige le code appelant à envelopper systématiquement l’opération dans un bloc try/catch, y compris pour des cas parfaitement prévisibles. Cette approche fonctionne, mais elle mélange deux catégories d’échecs : ceux qui relèvent d’un bogue (mauvaise configuration, erreur de programmation) et ceux qui relèvent du fonctionnement normal d’une API externe soumise à des limites.
Une exception devrait signaler l’imprévu. Un quota de requêtes atteint n’est pas imprévu : c’est un état que la fonction appelante doit pouvoir traiter comme un cas normal de son flux, au même titre qu’un succès.
Définir un type de retour explicite
Une classe de valeur dédiée, combinée à un union type, permet d’exprimer les deux issues dans la signature elle-même :

final class ReponseLlm
{
public function __construct(
public readonly string $texte,
public readonly int $jetonsUtilises
) {}
}
final class ErreurLlm
{
public function __construct(
public readonly string $code,
public readonly string $message
) {}
}
function appellerLlm( string $prompt ): ReponseLlm|ErreurLlm
{
$reponse = wp_remote_post( 'https://api.exemple-llm.test/v1/completions', array(
'body' => wp_json_encode( array( 'prompt' => $prompt ) ),
) );
if ( is_wp_error( $reponse ) ) {
return new ErreurLlm( 'reseau', $reponse->get_error_message() );
}
$code = wp_remote_retrieve_response_code( $reponse );
if ( 429 === (int) $code ) {
return new ErreurLlm( 'quota', 'Limite de requêtes atteinte' );
}
$donnees = json_decode( wp_remote_retrieve_body( $reponse ), true );
if ( empty( $donnees['text'] ) ) {
return new ErreurLlm( 'reponse_vide', 'Aucun texte retourné' );
}
return new ReponseLlm( $donnees['text'], $donnees['usage']['total_tokens'] ?? 0 );
}
Distinguer les cas côté appelant
Le code appelant utilise instanceof pour discriminer les deux issues, sans jamais avoir à deviner la nature de la valeur reçue :
$resultat = appellerLlm( $prompt );
if ( $resultat instanceof ErreurLlm ) {
error_log( sprintf( 'Échec LLM [%s] : %s', $resultat->code, $resultat->message ) );
return;
}
update_post_meta( $post_id, '_texte_genere', $resultat->texte );
Cette écriture rend visible, dès la lecture de la signature, que la fonction peut renvoyer deux formes de données différentes. Un développeur qui découvre le code n’a pas besoin de lire l’implémentation pour savoir qu’il doit gérer les deux branches.
Quand garder l’exception
Les exceptions restent adaptées aux erreurs de programmation : un prompt vide passé par erreur, une clé de configuration absente. Ces cas signalent un défaut du code appelant, pas un aléa du service distant. La distinction entre les deux catégories est ce qui guide le choix entre union type et exception, plus que la nature technique de l’échec.
Limites de l’approche
Un union type à deux membres reste lisible. Au-delà de trois ou quatre cas distincts, la discrimination par instanceof devient verbeuse et un pattern différent, comme un objet de résultat unique portant un statut, prend souvent le relais plus proprement.
Il faut aussi veiller à la cohérence des propriétés en lecture seule : le mot-clé readonly, également disponible depuis PHP 8.1 sur les propriétés, garantit qu’une valeur de réponse ne sera pas modifiée après sa construction, ce qui renforce la fiabilité du type.
Ce qu’il faut retenir
Un union type entre deux classes de valeur documente, dans le code lui-même, qu’un appel à un modèle de langage a deux issues légitimes. Cette écriture évite de surcharger les exceptions avec des cas prévisibles, et rend le traitement des erreurs explicite à la lecture, sans documentation supplémentaire.