Aller au contenu

Les erreurs comme fonctionnalité

Spec: ISO 9241-110, §5.6.4 Evidence: Code-backed

NextPDF traite sa hiérarchie d’exceptions comme une surface d’API, conçue avec le même soin que les méthodes qui les lèvent. Une défaillance est précise, typée, interceptable au niveau de granularité dont tu as besoin, et porte un contexte structuré pour tes journaux.

Cette page présente cette surface telle qu’elle existe dans le code source du moteur lui-même : le type de base, les sous-classes typées, les constructeurs nommés qui lient une cause racine au message, et le contexte structuré que chaque exception NextPDF expose.

Un message d’erreur, c’est le moteur qui te parle au pire moment possible : en production, à 2 h du matin, avec un document qui aurait dû partir. Ce que le message dit à ce moment-là détermine si l’étape suivante est un correctif ou une longue enquête.

Une RuntimeException: something went wrong générique ne te mène nulle part. Elle te dit que le moteur a échoué, mais pas ce qui a échoué, ni où, et certainement pas quoi faire. Les recommandations sur les facteurs humains sont directes à ce sujet. Une erreur devrait s’expliquer suffisamment clairement pour que la corriger soit l’étape suivante évidente, et non un projet de recherche ( Spec: ISO 9241-110, §5.6.4.3 ). Une exception qui nomme la cause et le remède n’est pas un luxe. C’est la différence entre un correctif de cinq minutes et un correctif de cinq heures.

  • Chaque défaillance NextPDF étend une seule base abstraite, NextPdfException, ce qui te permet d’intercepter toutes les erreurs de la bibliothèque avec un seul type.
  • Viennent ensuite des sous-classes précises et typées — une police introuvable, une configuration invalide, l’échec d’une opération de signature — afin que tu puisses intercepter exactement la défaillance que tu sais traiter.
  • Chaque exception NextPDF implémente ContextAwareExceptionInterface et expose getContext() : un tableau structuré, sûr pour les journaux, pour que tu n’aies jamais à analyser le texte d’un message afin de récupérer des diagnostics.
  • Les messages sont exploitables : les constructeurs nommés lient la cause racine réelle (et souvent le correctif) au message, au lieu d’un gabarit générique.
  • Chaque classe d’exception documente qui peut agir — développeur, infrastructure, ou appelant de la bibliothèque — afin que le triage commence avant que tu ne lises la trace de pile.

La hiérarchie est peu profonde et délibérée. Il y a une base, une couche de types spécifiques à un domaine, et un contrat que chacun d’eux respecte.

Une seule base, conçue comme filet de sécurité. NextPdfException est abstraite, étend RuntimeException, et implémente ContextAwareExceptionInterface :

abstract class NextPdfException extends RuntimeException implements ContextAwareExceptionInterface
{
/** @return array<string, mixed> */
public function getContext(): array
{
return [];
}
}

Le fait qu’elle soit abstraite est un choix. Cette base vague ne sera jamais levée directement par accident. Tu l’intercepteras délibérément, comme filet de sécurité, et tu intercepteras une sous-classe précise quand tu peux faire quelque chose de précis.

Sous-classes précises et typées. Une police manquante n’est pas une erreur générique ; c’est FontNotFoundException, et elle contient les données dont tu as besoin pour agir :

final class FontNotFoundException extends NextPdfException
{
public function __construct(
private readonly string $fontName,
private readonly array $searchPaths,
private readonly bool $fallbackAttempted,
?Throwable $previous = null,
) {
parent::__construct(
\sprintf('Font "%s" not found. Searched: [%s].', $fontName, \implode(', ', $searchPaths)),
0,
$previous,
);
}
// getFontName(), getSearchPaths(), wasFallbackAttempted(), getContext()
}

Le message nomme la police et les chemins exacts parcourus. Tu n’as pas à deviner quel répertoire manquait. L’exception te le dit.

Contexte structuré, pas extraction depuis une chaîne. Chaque exception renvoie un tableau en snake_case, composé uniquement de primitives, sûr à sérialiser directement dans un journal ou une charge utile APM :

public function getContext(): array
{
return [
'config_key' => $this->configKey,
'given_value' => $this->givenValue,
'expected_type' => $this->expectedType,
];
}

Le contrat explicite la raison d’être. Un intergiciel de journalisation peut appeler $logger->error($e->getMessage(), $e->getContext()) pour toute exception NextPDF sans jamais analyser le message. Le message est destiné aux humains. Le contexte est destiné aux machines. Ni l’un ni l’autre n’a vocation à jouer le rôle de l’autre.

Des messages exploitables via des constructeurs nommés. C’est là que les erreurs cessent d’être accessoires et deviennent un élément de conception à part entière. SignatureException ne dit pas seulement « la signature a échoué au niveau B-LT ». Elle propose des constructeurs nommés qui lient la cause racine réelle, et souvent le remède exact, au message :

public static function tsaUrlEmpty(string $signatureLevel): self
{
return new self('', $signatureLevel, null,
'TSA endpoint URL is empty: pass a non-empty `tsaUrl` to the TsaClient '
. 'constructor (e.g. "https://timestamp.example.com/tsa") or remove the '
. 'TSA client wiring if no timestamping is required at this signature level');
}

Le message indique ce qui ne va pas et quoi faire à ce sujet. Il existe des constructeurs équivalents pour un paquet de capacités manquant, un client HTTP absent, un algorithme à empreinte seule choisi par erreur, un type de clé qui ne correspond pas à l’algorithme, et d’autres encore. Chacun transforme une catégorie de défaillance en une phrase sur laquelle un développeur peut agir sans lire le code source du moteur.

Des défaillances bruyantes à dessein. Certaines exceptions existent précisément pour qu’une lacune silencieuse devienne bruyante. NotImplementedException porte une étiquette feature lisible par machine ainsi qu’une référence followUp :

final class NotImplementedException extends NextPdfException
{
public function __construct(
public readonly string $feature,
public readonly string $followUp,
?Throwable $previous = null,
) {
parent::__construct(
\sprintf('%s is not implemented in this release. %s', $feature, $followUp),
0, $previous,
);
}
}

Un chemin de code atteint mais pas encore raccordé lève cette exception au lieu de renvoyer un résultat neutre plausible. La même idée anime StrictModeViolation, dont les sous-classes portent une courte étiquette repérable identifiant la construction déviante, plus un contexte facultatif de localisation et de citation. Une déviation par rapport à la spécification devient un échec typé et contextualisé, et non un rendu discrètement faux.

Des métadonnées de triage dans la classe elle-même. Chaque classe d’exception nomme qui peut agir dans son docblock. Par exemple, FontNotFoundException est « Développeur (vérifier le chemin de la police) ou Infrastructure (corriger les permissions de fichier) ». InvalidConfigException est « Développeur (corriger la configuration avant d’appeler NextPDF) ». NotImplementedException est « Appelants de la bibliothèque — soit retirer l’appel, soit l’épingler à une version future ». Le triage commence avant la trace de pile, car la question « est-ce mon problème ou celui de l’exploitation ? » a déjà une réponse écrite noir sur blanc.

Le tableau résume la conception et ce que chaque propriété t’apporte.

Propriété de conceptionDans le code sourceCe que cela t’apporte
Une seule base abstraiteNextPdfException (abstraite, implémente l’interface de contexte)Intercepter toute erreur de la bibliothèque avec un seul type, sans jamais voir la base vague levée par accident
Sous-classes typées précisesFontNotFoundException, InvalidConfigException, SignatureException, …Intercepter exactement la défaillance que tu sais traiter
Contexte structurégetContext() — primitives en snake_case uniquementJournaliser ou envoyer vers l’APM sans analyser le texte d’un message
Messages exploitablesLes constructeurs nommés lient cause racine + remèdeUne phrase sur laquelle agir, pas un gabarit
Bruyant à desseinNotImplementedException, StrictModeViolationUne lacune silencieuse devient un arrêt typé et repérable
Métadonnées de tri« Actionable by :  » dans le docblock de chaque classeSavoir à qui revient le problème avant de lire la trace

Cette page est Evidence: Code-backed  : chaque classe, signature et forme de message est tirée de l’espace de noms d’exceptions du moteur, et non paraphrasée.

  • La base abstraite et son contrat ContextAwareExceptionInterface, les sous-classes typées, la forme de getContext() et les constructeurs nommés de SignatureException sont cités textuellement à partir du code source.
  • Les lignes de triage « Actionable by :  » sont des contrats de docblock de classe dans ces mêmes fichiers.
  • L’ancrage relatif aux facteurs humains est Spec: ISO 9241-110 — §5.6.4.3, sur les erreurs qui s’expliquent assez pour être corrigées, ainsi que le principe de robustesse face aux erreurs d’usage du §6. Le moteur traite le développeur comme l’utilisateur et l’exception comme l’interface qui doit satisfaire ces clauses.

Intercepte largement pour le filet de sécurité, et précisément là où tu peux agir, puis transmets le contexte structuré directement à ton journaliseur — sans analyser le message.

<?php
declare(strict_types=1);
use NextPDF\Core\Document;
use NextPDF\Exception\FontNotFoundException;
use NextPDF\Exception\NextPdfException;
use Psr\Log\LoggerInterface;
function renderInvoice(LoggerInterface $logger): ?string
{
try {
$document = Document::createStandalone();
$document->setTitle('Invoice 2026-0042');
$document->addPage();
$document->setFont('BrandSans', '', 12);
$document->cell(0, 10, 'Thank you for your business.', newLine: true);
return $document->getPdfData();
} catch (FontNotFoundException $e) {
// Specific: we can recover — fall back to a built-in font.
// getContext() is log-safe structured data, not a parsed string.
$logger->warning($e->getMessage(), $e->getContext());
return null; // caller re-renders with 'helvetica'
} catch (NextPdfException $e) {
// Backstop: any other NextPDF failure, still with structured context.
$logger->error($e->getMessage(), $e->getContext());
return null;
}
}

Le catch précis peut récupérer parce que le type d’exception lui a indiqué que la récupération était possible. Le filet de sécurité journalise le contexte structuré pour tout le reste. À aucun moment l’application ne lit le message pour découvrir ce qui s’est passé.

Une lecture erronée courante consiste à penser qu’un arbre d’exceptions profond relève de la sur-ingénierie, et qu’un seul type d’erreur serait plus simple. Ce serait plus simple pour le moteur et pire pour toi. Un seul type signifie que chaque défaillance se résume à une trace de pile générique et que la logique de récupération repose sur de la correspondance de chaînes. Cette correspondance est fragile ; la prochaine reformulation du message la casse. Une hiérarchie resserrée et précise déplace cette connaissance dans le système de types, où le compilateur et tes blocs catch peuvent l’exploiter.

Une deuxième idée fausse est que le message et le contexte sont redondants. Ce n’est pas le cas. Le message est de la prose pour un humain lisant une ligne de journal. Le contexte est un tableau typé pour le routage de code, les alertes ou les tableaux de bord. Les confondre, c’est exactement le piège de l’analyse de chaînes que le contrat getContext() existe pour supprimer.

La hiérarchie est intentionnellement peu profonde. NextPDF ne crée pas une classe d’exception distincte pour chaque défaillance concevable. Il en crée une lorsque intercepter précisément cette défaillance correspond à ce qu’un appelant ferait raisonnablement. Un découpage trop fin remplacerait le problème de l’analyse de chaînes par celui d’une liste de catch tentaculaire.

getContext() est structuré pour les journaux et l’APM, il ne renvoie donc que des primitives et des listes de primitives, sans objets imbriqués, par contrat. C’est un contexte de diagnostic, pas un instantané sérialisé des composants internes du moteur. Ce n’est pas non plus un format de transport stable sur lequel bâtir des schémas externes.

Cette page décrit la surface de conception des exceptions. L’ensemble exact des exceptions et de leurs champs évolue avec le moteur. Les classes et formes citées ici sont à jour au moment de cette revue et illustrent le contrat, sans constituer un catalogue figé. Le contrat — une base, des sous-classes typées, un contexte structuré, des messages exploitables — est la partie stable.

  • Adossé au code (niveau de preuve) — une page dont les affirmations sont vérifiées par rapport au code source du moteur, avec des citations plutôt que de simples paraphrases.
  • Exception consciente du contexte — une exception NextPDF qui implémente ContextAwareExceptionInterface et expose getContext(). Cette méthode renvoie un tableau en snake_case de champs de diagnostic primitifs, sûr à sérialiser dans un journal ou une charge utile APM sans analyser la chaîne du message.
  • Constructeur nommé — une méthode de fabrique statique (par exemple SignatureException::tsaUrlEmpty()) qui construit une exception avec un message lié à une cause racine précise et, souvent, à son remède.
  • PAdES — PDF Advanced Electronic Signatures, la famille de profils ETSI pour la signature de PDF. Développé à la première occurrence ; traité en profondeur dans les pages consacrées à la signature.
  • TSA — Time-Stamping Authority, le service de confiance qui émet les horodatages RFC 3161 utilisés par les profils PAdES supérieurs.