Mettre en place une récupération après erreur et des stratégies de réessai sur mesure
En un coup d’œil
Section intitulée « En un coup d’œil »Un service de documents de production ne se contente pas d’intercepter une exception et de la journaliser. Il décide quoi faire ensuite : continuer avec un résultat dégradé, basculer vers un second chemin de rendu, réessayer avec une entrée que le moteur accepte, ou livrer les pages déjà construites avant l’échec. Cette recette présente quatre stratégies de récupération bâties sur la hiérarchie d’exceptions NextPDF et les méthodes d’inspection de l’état du document :
- Dégradation contrôlée lors d’un échec de police — intercepte
NextPDF\Exception\FontNotFoundException, rabats-toi sur une fonte garantie et poursuis la construction du document. - Un renderer de repli — lorsque le chemin in-process
Document::writeHtml()rejette l’entrée, réessaie viaDocument::writeHtmlChrome(), le pont Chrome denextpdf/artisan. - Réessai avec un HTML alternatif — lorsque
NextPDF\Exception\HtmlParsingExceptionouNextPDF\Exception\CssResolutionBudgetExceededExceptionest déclenchée, réessaie avec une variante HTML simplifiée et éprouvée. - Récupération de document partiel — lis
Document::getNumPages()après un échec et sauvegarde ce qui a déjà été construit au lieu de le jeter.
Tu sais déjà intercepter à la bonne granularité. La page compagnon Gérer les erreurs avec la hiérarchie d’exceptions NextPDF couvre la hiérarchie elle-même. Cette page couvre ce que tu fais après l’interception.
Cette recette vise l’édition cœur OSS. Chaque API mentionnée ici se trouve dans nextpdf/core. La seule dépendance optionnelle est nextpdf/artisan pour le repli Chrome.
Installation
Section intitulée « Installation »composer require nextpdf/core:^3La stratégie du renderer de repli utilise aussi le pont Chrome :
composer require nextpdf/artisanLorsque nextpdf/artisan est absent, Document::writeHtmlChrome() lève NextPDF\Exception\PageLayoutException au lieu d’effectuer le rendu ; la stratégie de repli ci-dessous traite donc un pont manquant comme un cas récupérable supplémentaire.
Vue d’ensemble conceptuelle
Section intitulée « Vue d’ensemble conceptuelle »La récupération repose sur deux faits concernant NextPDF, tous deux vérifiés dans le code source.
La hiérarchie d’exceptions t’indique ce qui est récupérable. Chaque exception métier étend la base abstraite NextPDF\Exception\NextPdfException, qui étend RuntimeException et implémente NextPDF\Contracts\ContextAwareExceptionInterface. Intercepte un sous-type spécifique pour choisir un chemin de récupération adapté à l’échec :
FontNotFoundExceptionportegetFontName(),getSearchPaths(), etwasFallbackAttempted()— ce qui permet de réessayer avec une fonte différente.HtmlParsingExceptionportegetRule(),getPosition(), etgetHtmlSnippet()— ce qui permet de décider si un réessai simplifié vaut la peine d’être tenté.CssResolutionBudgetExceededExceptionportegetVisits()etgetBudget()— ce qui signale un sélecteur pathologique qu’une feuille de style allégée peut corriger.- Limite importante :
NextPDF\Support\DegradedExceptionétendRuntimeExceptiondirectement, et nonNextPdfException. Par conséquent,catch (NextPdfException $e)n’intercepte pas un rejet de politique de dégradation. Lorsque laNextPDF\Contracts\DegradationPolicyactive vautStrictouBalanced, intercepte explicitementDegradedExceptionpour pouvoir récupérer.
Le document est inspectable pendant que tu le construis. Un Document expose son état de construction via des accesseurs en lecture seule. getNumPages() renvoie le nombre total de pages, y compris la page active non vidée, et getPage() renvoie l’index basé sur zéro de la page courante. Après un échec en cours de construction, lis getNumPages() pour savoir s’il existe des pages complètes, puis appelle save() ou getPdfData() pour les émettre. Le moteur enregistre aussi les événements de dégradation non fatals : getWarnings() renvoie une list<NextPDF\Support\Warning>, hasWarnings() indique si des avertissements ont été collectés, et hasDegradedParity() indique si la fidélité de sortie a été affectée. Cela permet à une routine de récupération de distinguer « réussi proprement » de « réussi avec une fidélité réduite » sans avoir à analyser une exception.
La politique de dégradation détermine quels événements tu traites comme des exceptions plutôt que comme des avertissements. NextPDF\Core\Config a pour valeur par défaut DegradationPolicy::Balanced, qui avertit et continue pour une dégradation bornée, mais lève en cas d’impact bloquant. DegradationPolicy::Permissive ne lève jamais et collecte tout dans le canal d’avertissements. DegradationPolicy::Strict lève en cas de risque de conformité, de perte sémantique ou d’impact bloquant. Choisis d’abord la politique, puis écris la récupération pour les formes d’échec qu’elle produit.
Surface de l’API
Section intitulée « Surface de l’API »Le code de récupération ci-dessous utilise ces membres vérifiés :
NextPDF\Core\Document::createStandalone(?Config $config = null): self,addPage(),setFont(string $family, string $style = '', float $size = 12.0): static,cell(...),writeHtml(string $html): static,writeHtmlChrome(string $html, ?float $width = null, ?float $height = null): static,save(string $path): void,getPdfData(): string,getNumPages(): int,getPage(): int,getWarnings(): list<Warning>,hasWarnings(): bool,hasDegradedParity(): bool,addFontDirectory(string $directory): static.NextPDF\Core\Config::withDegradationPolicy(DegradationPolicy $policy): selfet la valeur par défautdegradationPolicydeDegradationPolicy::Balanced.NextPDF\Contracts\DegradationPolicy—Strict,Balanced,Permissive.NextPDF\Exception\NextPdfException(base abstraite),NextPDF\Exception\FontNotFoundException,NextPDF\Exception\HtmlParsingException,NextPDF\Exception\CssResolutionBudgetExceededException,NextPDF\Exception\WriterException,NextPDF\Exception\PageLayoutException.NextPDF\Support\DegradedException(portantcapabilityetpolicy),NextPDF\Support\Capability(id,status,reason,isDegraded()),NextPDF\Support\Warning,NextPDF\Support\WarningSeverity.
Exemple de code — Démarrage rapide
Section intitulée « Exemple de code — Démarrage rapide »La récupération utile minimale : intercepte une police introuvable, rabats-toi sur une fonte garantie et continue. Ce fragment laisse de côté la gestion plus large de l’exemple de production. Pour un gestionnaire complet avec journalisation et prise en compte de la limite DegradedException, lis l’exemple de production ci-dessous.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Exception\FontNotFoundException;
$doc = Document::createStandalone();$doc->addPage();
try { // A face that may not be installed on every host. $doc->setFont('CorporateSans', '', 12);} catch (FontNotFoundException $e) { // Recover: fall back to a face the engine always resolves. $doc->setFont('helvetica', '', 12);}
$doc->cell(0, 10, 'Rendered with a recovered font.', newLine: true);$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');Exemple de code — Production
Section intitulée « Exemple de code — Production »L’exemple complet câble les quatre stratégies dans un seul pipeline de rendu : un repli de police, un repli de renderer du chemin in-process vers Chrome, un réessai avec HTML alternatif et une récupération de document partiel pilotée par getNumPages(). Il respecte le canal de sortie du harnais, n’intercepte jamais une Exception nue et ne laisse jamais un bloc catch vide.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Contracts\ContextAwareExceptionInterface;use NextPDF\Contracts\DegradationPolicy;use NextPDF\Core\Config;use NextPDF\Core\Document;use NextPDF\Exception\CssResolutionBudgetExceededException;use NextPDF\Exception\FontNotFoundException;use NextPDF\Exception\HtmlParsingException;use NextPDF\Exception\NextPdfException;use NextPDF\Exception\PageLayoutException;use NextPDF\Exception\WriterException;use NextPDF\Support\DegradedException;
/** * A minimal structured sink. In production this is your PSR-3 logger; the * exception class and its structured context become log fields. * * @param array<string, mixed> $context */function logRecovery(string $message, array $context): void{ fwrite(STDERR, $message . ' ' . json_encode($context, JSON_THROW_ON_ERROR) . "\n");}
/** * Resolve a usable font, degrading from the requested face to a guaranteed * fallback. Returns the face actually applied so the caller can record it. * * @param non-empty-string $requested * @param non-empty-string $fallback * * @return non-empty-string */function applyFontWithFallback(Document $doc, string $requested, string $fallback): string{ try { $doc->setFont($requested, '', 12);
return $requested; } catch (FontNotFoundException $e) { // STRATEGY 1 — graceful degradation on a font failure. logRecovery('Font unavailable; degrading to a guaranteed face', [ 'exception' => $e::class, 'font_name' => $e->getFontName(), 'searched' => $e->getSearchPaths(), 'fallback' => $fallback, ]); $doc->setFont($fallback, '', 12);
return $fallback; }}
/** * Render HTML through the in-process pipeline, then through the Chrome bridge, * then through a simplified HTML variant. Each layer recovers a more specific * failure than the last. */function renderHtmlWithRecovery(Document $doc, string $primaryHtml, string $simplifiedHtml): void{ try { // Primary path: the in-process HTML/CSS pipeline. $doc->writeHtml($primaryHtml);
return; } catch (CssResolutionBudgetExceededException $e) { // STRATEGY 3 — retry with alternative HTML for a pathological selector. logRecovery('CSS resolution budget exceeded; retrying with simplified HTML', [ 'exception' => $e::class, 'visits' => $e->getVisits(), 'budget' => $e->getBudget(), ]); $doc->writeHtml($simplifiedHtml);
return; } catch (HtmlParsingException $e) { // STRATEGY 2 — fall back to the Chrome renderer for input the // in-process parser rejects. The Chrome bridge uses a browser CSS // engine, so it may accept what the in-process parser would not. logRecovery('In-process HTML parse failed; trying the Chrome fallback renderer', [ 'exception' => $e::class, 'rule' => $e->getRule(), 'position' => $e->getPosition(), ]);
try { $doc->writeHtmlChrome($primaryHtml);
return; } catch (PageLayoutException $chromeError) { // The Chrome bridge is absent (nextpdf/artisan not installed) or // rejected the input. Last resort: the simplified HTML variant // through the in-process pipeline. logRecovery('Chrome fallback unavailable; retrying with simplified HTML', [ 'exception' => $chromeError::class, ]); $doc->writeHtml($simplifiedHtml);
return; } }}
// --- Configure the degradation policy up front ---------------------------// Balanced (the default) warns on bounded degradation and throws only on a// blocking impact. A regulated workflow would choose DegradationPolicy::Strict.$config = (new Config())->withDegradationPolicy(DegradationPolicy::Balanced);$doc = Document::createStandalone($config);
$primaryHtml = '<h1>Quarterly report</h1><p>Body paragraph with rich styling.</p>';$simplifiedHtml = '<h1>Quarterly report</h1><p>Body paragraph (simplified).</p>';
$outputPath = getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf';
try { $doc->addPage(); $applied = applyFontWithFallback($doc, 'CorporateSans', 'helvetica'); $doc->cell(0, 12, 'Custom error recovery patterns', newLine: true);
renderHtmlWithRecovery($doc, $primaryHtml, $simplifiedHtml);
$doc->save($outputPath);
logRecovery('Document built', [ 'font_applied' => $applied, 'pages' => $doc->getNumPages(), 'has_warnings' => $doc->hasWarnings(), 'degraded_parity' => $doc->hasDegradedParity(), ]);} catch (DegradedException $e) { // BOUNDARY: DegradedException extends RuntimeException directly, NOT // NextPdfException, so the catch-all below would not have caught it. // Under Strict/Balanced policy a blocking degradation lands here. logRecovery('Capability degraded under the active policy; emitting a built partial', [ 'exception' => $e::class, 'capability' => $e->capability->id, 'status' => $e->capability->status->value, 'reason' => $e->capability->reason ?? 'unknown', 'policy' => $e->policy->value, ]); // STRATEGY 4 — partial-document recovery: save whatever pages exist. if ($doc->getNumPages() > 0) { $doc->save($outputPath); }} catch (WriterException $e) { // Serialization or I/O failure: the in-memory document is valid but could // not be written. Surface the stage so infrastructure can act on it. logRecovery('PDF write failed; document was valid in memory', [ 'exception' => $e::class, 'writer_state' => $e->getWriterState(), 'output_path' => $e->getOutputPath(), ]);} catch (NextPdfException $e) { // Catch-all for every other NextPDF\Exception\*. STRATEGY 4 again: if any // complete pages were built before the failure, emit them rather than // discarding the work. $context = ['exception' => $e::class, 'pages' => $doc->getNumPages()]; if ($e instanceof ContextAwareExceptionInterface) { $context += $e->getContext(); } logRecovery('Unrecovered NextPDF failure; attempting a partial save', $context); if ($doc->getNumPages() > 0) { $doc->save($outputPath); }}
fwrite(STDERR, "Recovery pipeline complete.\n");STDOUT reste libre pour le harnais. Les diagnostics de récupération vont vers STDERR, et le PDF est écrit uniquement vers NEXTPDF_COOKBOOK_OUTPUT.
Cas limites et pièges
Section intitulée « Cas limites et pièges »- Ordonne les blocs catch du spécifique au général. PHP utilise le premier
catchcompatible. Placercatch (NextPdfException $e)avantcatch (WriterException $e)transforme le bloc spécifique en code mort, carWriterExceptionétendNextPdfException. DegradedExceptionse situe en dehors de la hiérarchie. Elle étendRuntimeException, et nonNextPdfException. Un pipeline qui n’intercepte queNextPdfExceptionlaisse un rejet de politique stricte se propager sans interception. IntercepteDegradedException(ou unRuntimeExceptionplus large) lorsqu’une politique de dégradation autre que celle par défaut est active.- Un repli de police peut échouer lui aussi. Si ta fonte de repli n’est pas enregistrée non plus, le second
setFont()lève à nouveau. Utilise un alias Base14 tel quehelvetica, que le moteur résout sans recherche dans le système de fichiers, ou enregistre au démarrage une fonte fournie viaaddFontDirectory()pour que le repli soit garanti. getNumPages()compte la page active non vidée. Elle renvoie le nombre de pages vidées, plus un lorsqu’une page est actuellement ouverte. Ainsi, une « sauvegarde partielle » inclut la page qui était en cours de construction quand l’échec s’est produit, ce qui est généralement ce que tu veux. Si tu n’as besoin que des pages entièrement complètes, branche-toi aussi surgetPage().- Le repli Chrome change la fidélité, pas seulement la disponibilité. Le pipeline in-process et le pont Chrome utilisent des moteurs de mise en page différents ; un document qui se rabat sur Chrome peut donc avoir un aspect différent. Traite le repli comme une récupération, pas comme un substitut transparent, et note quel chemin a produit la sortie.
- Un réessai doit utiliser une entrée éprouvée. Le réessai en HTML simplifié n’aide que lorsque la variante simplifiée est réellement plus simple — moins de sélecteurs imbriqués, pas de chaînes
:has()qui épuisent le budget de résolution. Réessayer avec la même entrée qui a déjà échoué revient à boucler sur la même exception. - Inspecte les avertissements après une exécution propre. Un rendu qui se termine sans lever d’exception peut quand même avoir subi une dégradation. Vérifie
hasDegradedParity()et lisgetWarnings()avant de considérer la sortie comme fidèle au pixel ; sousDegradationPolicy::Permissive, chaque dégradation est un avertissement, jamais une exception.
Performances
Section intitulée « Performances »- La récupération n’ajoute un coût que sur le chemin d’échec. NextPDF lève lors d’états exceptionnels ; un rendu propre ne paie donc rien pour le
try/catchqui l’entoure. - Un repli de renderer relance le rendu. La tentative in-process est jetée et la tentative Chrome repart de zéro ; un rendu de repli coûte donc, dans le pire des cas, les deux temps de rendu plus l’aller-retour inter-processus vers Chrome. Prévois-le dans ton budget quand tu définis les délais d’expiration de requête.
- Un réessai avec HTML alternatif analyse un second document. Garde la variante simplifiée courte pour que le réessai reste peu coûteux par rapport à la tentative primaire.
- Une sauvegarde partielle sérialise les pages déjà construites. Son coût dépend du nombre de pages survivantes, pas du travail qui a échoué.
Notes de sécurité
Section intitulée « Notes de sécurité »- N’expose pas les messages d’exception bruts ni les chemins du système de fichiers aux utilisateurs finaux. Un message
FontNotFoundExceptioninclut les répertoires recherchés et uneWriterExceptioninclut le chemin de sortie ; les deux divulguent l’organisation du serveur. Journalise le contexte structuré côté serveur et renvoie un message générique à l’appelant. - Traite le HTML de chaque tentative comme une entrée non fiable. Le repli et le réessai en HTML simplifié transitent tous deux par la même frontière d’entrée ; le pipeline in-process et le pont Chrome appliquent chacun leur propre politique de sécurité HTML, et un réessai n’assouplit pas cette validation. Ne suppose pas qu’une variante « simplifiée » est plus sûre parce que tu l’as rédigée toi-même.
- Une sauvegarde partielle écrit quand même un fichier. Applique à une sortie partielle les mêmes règles de validation de chemin, de permissions et d’emplacement de stockage qu’à une sortie complète.
Document::save()rejette les wrappers de flux et les octets nuls et résout le répertoire parent pour bloquer la traversée de chemin, mais la destination que tu passes relève de ta responsabilité.
Conformité
Section intitulée « Conformité »Cette recette ne fait aucune revendication normative de standard. Elle compose les API publiques d’exceptions et d’inspection de document de NextPDF dans un flux de contrôle de récupération ; elle n’affirme aucun comportement défini par ISO 32000-2 ni aucun autre standard, donc elle ne porte aucun bloc citations:.
Il est vérifié avec le profil de reproductibilité sémantique. Le document récupéré porte un /ID de trailer et une date de modification régénérés à chaque sauvegarde ; l’identité à l’octet n’est donc pas atteignable. La comparaison de l’AST structurel plus les seules métadonnées est stable d’une exécution à l’autre.
Voir aussi
Section intitulée « Voir aussi »- Gérer les erreurs avec la hiérarchie d’exceptions NextPDF — granularité d’interception et contexte structuré, le socle sur lequel cette page s’appuie.
- Module Exception — la référence complète des exceptions.
- Module Support —
DegradedException,Capability,Warning, et les types de dégradation. - Module Config — configuration de la politique de dégradation.
- Rendre des PDF en toute sécurité dans un worker longue durée — récupération dans un worker qui réutilise des registres partagés.