Aller au contenu

Gérer les erreurs avec la hiérarchie d'exceptions de NextPDF

NextPDF lève des exceptions typées pour signaler les états exceptionnels. Il ne masque jamais une erreur derrière un retour false ou null. Chaque exception de domaine hérite d’une base abstraite commune, NextPdfException, et expose un contexte de diagnostic structuré via ContextAwareExceptionInterface. Ce recipe te montre comment intercepter les exceptions au bon niveau de granularité et journaliser le contexte structuré dans une chaîne de supervision applicative (APM). Il signale aussi les défaillances qu’un unique bloc attrape-tout ne couvre pas.

Fenêtre de terminal
composer require nextpdf/core:^3

Aucune extension supplémentaire n’est requise.

La hiérarchie est la suivante :

RuntimeException
└── NextPdfException (abstract, implements ContextAwareExceptionInterface)
├── InvalidConfigException
├── FontNotFoundException
├── FontParsingException
├── ImageProcessingException
├── WriterException
├── SignatureException
├── EncryptionException
├── HtmlParsingException
├── … (every domain exception under NextPDF\Exception)
└── Strict\StrictModeViolation (abstract)
├── Strict\IncompatibleRenderingModeException
└── Strict\OracleConformanceFailure

Cette hiérarchie a deux conséquences pratiques, toutes deux vérifiées par rapport au code source :

  1. catch (NextPdfException $e) intercepte chaque exception sous NextPDF\Exception, y compris les violations du mode strict. Elles héritent toutes de la base abstraite.
  2. Il n’intercepte pas tout ce que la bibliothèque peut lever. NextPDF\Support\DegradedException étend RuntimeException directement, et non NextPdfException. Un catch (NextPdfException $e) n’intercepte donc pas un rejet de la politique de dégradation. Pour gérer ce cas, intercepte explicitement DegradedException (ou le plus large RuntimeException). Ce recipe explicite cette frontière au lieu de prétendre qu’un seul bloc attrape-tout couvre tout.

NextPdfException::getContext() renvoie un array<string, mixed> dont les clés sont en snake_case et dont les valeurs sont uniquement des primitives (ou des listes de primitives). Tu peux donc le sérialiser directement dans le tableau de contexte d’un logger PSR-3. PSR-3 §1.3 place une exception sous la clé de contexte 'exception'. NextPDF, avec son getContext(), y ajoute des détails de domaine, et non l’objet exception lui-même.

Cette surface d’API est générée à partir du PHPDoc de NextPDF\Exception\NextPdfException, de NextPDF\Contracts\ContextAwareExceptionInterface, des exceptions de domaine concrètes (par exemple NextPDF\Exception\FontNotFoundException, avec getFontName() / getSearchPaths() / wasFallbackAttempted()), et de NextPDF\Support\DegradedException (qui porte les Capability et DegradationPolicy). Les membres utilisés ci-dessous sont NextPdfException::getContext() et les accesseurs propres à chaque exception.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Exception\NextPdfException;
try {
$doc = Document::createStandalone();
$doc->addPage();
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 10, 'Hello');
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');
} catch (NextPdfException $e) {
// Every NextPDF\Exception\* (and strict-mode violation) lands here.
// $e->getContext() is APM-safe structured detail.
error_log($e->getMessage());
}

L’exemple complet illustre l’interception granulaire, la journalisation du contexte structuré et la frontière de DegradedException. Il respecte le canal de sortie du harnais.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Contracts\ContextAwareExceptionInterface;
use NextPDF\Exception\FontNotFoundException;
use NextPDF\Exception\NextPdfException;
use NextPDF\Support\DegradedException;
/**
* A minimal PSR-3-shaped sink. In production this is your real logger;
* the exception goes under the 'exception' key (PSR-3 §1.3) and the
* NextPDF structured context is merged in as domain detail.
*
* @param array<string, mixed> $context
*/
function logError(string $message, array $context): void
{
fwrite(STDERR, $message . ' ' . json_encode($context, JSON_THROW_ON_ERROR) . "\n");
}
$doc = Document::createStandalone();
$doc->setTitle('Exception handling patterns');
try {
$doc->addPage();
$doc->setFont('helvetica', 'B', 16);
$doc->cell(0, 12, 'Exception-aware error handling', newLine: true);
// This call succeeds; the catch blocks below show the SHAPE of handling.
$doc->setFont('helvetica', '', 11);
$doc->cell(0, 8, 'Catch specifically, then fall back to the base.', newLine: true);
} catch (FontNotFoundException $e) {
// Most specific first: actionable, typed accessors.
logError('Font missing — using a fallback face', [
'exception' => $e::class,
'font_name' => $e->getFontName(),
'searched' => $e->getSearchPaths(),
'fallback' => $e->wasFallbackAttempted(),
]);
} catch (NextPdfException $e) {
// Catch-all for every NextPDF\Exception\* including strict violations.
$context = ['exception' => $e::class];
if ($e instanceof ContextAwareExceptionInterface) {
$context += $e->getContext();
}
logError($e->getMessage(), $context);
} catch (DegradedException $e) {
// BOUNDARY: DegradedException extends RuntimeException directly, NOT
// NextPdfException. The catch above would NOT have caught it. This
// explicit block (or a broader RuntimeException) is required.
logError('Capability degraded under the active policy', [
'exception' => $e::class,
'capability' => $e->capability->id,
'policy' => $e->policy->value,
]);
}
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');
fwrite(STDERR, "Document built; handlers wired.\n");

STDOUT reste libre pour le harnais ; le PDF n’est écrit que vers NEXTPDF_COOKBOOK_OUTPUT.

  • Ordonne les blocs catch du plus spécifique au plus général. PHP exécute le premier catch compatible. Un catch (NextPdfException $e) placé avant catch (FontNotFoundException $e) rend le bloc spécifique inaccessible (code mort).
  • DegradedException n’est pas une NextPdfException. Vérifié par rapport au code source, elle étend RuntimeException. Un seul catch (NextPdfException $e) laisse se propager silencieusement un rejet de dégradation stricte. Intercepte-le (ou RuntimeException) explicitement quand une politique de dégradation est en jeu.
  • getContext() est sûr pour l’APM par contrat. Les clés sont en snake_case. Les valeurs sont des primitives ou des listes de primitives, sans objets imbriqués ni ressources. Tu peux sérialiser le résultat directement, et il ne contient jamais d’octets de document.
  • Ne parse pas les messages d’exception. Les messages sont destinés aux humains et peuvent changer. Les accesseurs typés (getFontName(), capability->id, etc.) et getContext() constituent la surface machine stable.
  • Attention aux décomptes obsolètes. D’anciens supports peuvent citer un nombre fixe de « N exceptions de domaine ». La hiérarchie s’enrichit au fil des versions. Appuie-toi sur le type de base NextPdfException et sur instanceof, jamais sur un nombre codé en dur.
  • Les espaces réservés PSR-3 restent des chaînes. Lorsque tu journalises, garde le message sous forme de chaîne avec des jetons {placeholder} et place les valeurs dans le tableau de contexte (PSR-3 §1.2). N’interpole pas l’objet exception dans le message.

La gestion des exceptions n’ajoute aucun coût en fonctionnement normal. NextPDF ne lève des exceptions que dans les états exceptionnels, et getContext() construit un petit tableau à la demande. Le performance_budget (wall_ms: 2000, peak_mb: 96) borne l’exécution de ce recipe dans le harnais, et non celle de documents arbitraires.

  • getContext() est conçu pour être sûr à journaliser : uniquement des primitives, aucune charge utile de document, aucun octet de fichier. Tu restes responsable des valeurs que tu ajoutes à un contexte de journal. Nettoie toute valeur provenant de l’utilisateur (un chemin de fichier, par exemple) selon ta politique de journalisation avant qu’elle n’atteigne un puits.
  • N’affiche pas de messages d’exception bruts aux utilisateurs finaux d’une manière qui révélerait l’organisation du système de fichiers. Affiche un message générique et journalise le contexte structuré côté serveur.
DéclarationSpécificationArticlereference_id
Une exception a sa place dans le contexte de journal PSR-3 sous la clé exception.PSR-3§1.3
Les messages de journal restent des chaînes ; les noms d’espace réservé correspondent aux clés de contexte.PSR-3§1.2
La date de modification est régénérée à chaque enregistrement, donc la sortie est stable structurellement (et non octet pour octet).ISO 32000-2§14.3

Ce recipe est vérifié avec le profil de reproductibilité structurel. La sortie contient un /ID dans le trailer et une date de modification qui sont régénérés à chaque enregistrement ; l’identité octet pour octet n’est donc pas atteignable. La structure normalisée par qpdf est stable.