Ga naar inhoud

Aangepaste patronen implementeren voor foutherstel en opnieuw proberen

Een documentservice in productie doet meer dan een exception opvangen en loggen. Zo’n service bepaalt wat er daarna gebeurt: doorgaan met gedegradeerde uitvoer, overschakelen naar een tweede renderpad, opnieuw proberen met invoer die de engine accepteert, of de pagina’s leveren die vóór de fout zijn opgebouwd. Deze pagina toont vier herstelstrategieën die voortbouwen op de NextPDF exception-hiërarchie en op de methoden om de documentstatus te inspecteren:

  • Soepele degradatie bij een lettertypefout — vang NextPDF\Exception\FontNotFoundException op, val terug op een gegarandeerd lettertype en bouw het document verder op.
  • Een fallbackrenderer — als het in-procespad Document::writeHtml() de invoer weigert, probeer dan opnieuw via Document::writeHtmlChrome(), de Chrome-bridge van nextpdf/artisan.
  • Opnieuw proberen met alternatieve HTML — als NextPDF\Exception\HtmlParsingException of NextPDF\Exception\CssResolutionBudgetExceededException optreedt, probeer dan opnieuw met een vereenvoudigde, bekend-goede HTML-variant.
  • Herstel van een gedeeltelijk document — lees Document::getNumPages() na een fout en sla op wat al is opgebouwd in plaats van het weg te gooien.

Je weet al hoe je op het juiste niveau opvangt. De bijbehorende pagina Fouten afhandelen met de NextPDF exception-hiërarchie behandelt de hiërarchie zelf. Deze pagina laat zien wat je na het opvangen moet doen.

Deze pagina is gericht op de open source software (OSS)-core-editie. Elke application programming interface (API) die hier wordt genoemd, bevindt zich in nextpdf/core. De enige optionele afhankelijkheid is nextpdf/artisan voor de Chrome-fallback.

Terminal window
composer require nextpdf/core:^3

De fallbackrenderer-strategie gebruikt daarnaast de Chrome-bridge:

Terminal window
composer require nextpdf/artisan

Wanneer nextpdf/artisan ontbreekt, werpt Document::writeHtmlChrome() een NextPDF\Exception\PageLayoutException in plaats van te renderen. De onderstaande fallbackstrategie behandelt een ontbrekende bridge als een extra herstelbaar geval.

Herstel hangt af van twee feiten over NextPDF, die allebei tegen de broncode zijn geverifieerd.

De exception-hiërarchie laat zien wat herstelbaar is. Elke domeinexception breidt de abstracte basis NextPDF\Exception\NextPdfException uit, die op zijn beurt RuntimeException uitbreidt en NextPDF\Contracts\ContextAwareExceptionInterface implementeert. Vang een specifiek subtype op om voor die fout een herstelpad te kiezen:

  • FontNotFoundException bevat getFontName(), getSearchPaths() en wasFallbackAttempted() — genoeg om het opnieuw te proberen met een ander lettertype.
  • HtmlParsingException bevat getRule(), getPosition() en getHtmlSnippet() — genoeg om te beslissen of een vereenvoudigde poging de moeite waard is.
  • CssResolutionBudgetExceededException bevat getVisits() en getBudget() — een signaal dat een uitgeklede stylesheet een pathologische selector kan omzeilen.
  • Eén belangrijke grens: NextPDF\Support\DegradedException breidt RuntimeException rechtstreeks uit, niet NextPdfException. Daarom vangt catch (NextPdfException $e) een afwijzing door het degradatiebeleid niet op. Als het actieve NextPDF\Contracts\DegradationPolicy Strict of Balanced is, vang dan expliciet DegradedException op om ervan te herstellen.

Het document is inspecteerbaar terwijl je het opbouwt. Een Document stelt zijn opbouwstatus beschikbaar via alleen-lezenaccessors. getNumPages() retourneert het totale aantal pagina’s, inclusief de actieve, nog niet weggeschreven pagina, en getPage() retourneert de nulgebaseerde index van de huidige pagina. Lees na een fout midden in de opbouw getNumPages() om te achterhalen of er volledige pagina’s bestaan, en roep daarna save() of getPdfData() aan om ze weg te schrijven. De engine registreert ook niet-fatale degradatiegebeurtenissen: getWarnings() retourneert een list<NextPDF\Support\Warning>, hasWarnings() meldt of er waarschuwingen zijn verzameld, en hasDegradedParity() meldt of de getrouwheid van de uitvoer is aangetast. Met deze methoden kan een herstelroutine „schoon geslaagd” onderscheiden van „geslaagd met verminderde getrouwheid” zonder een exception te parsen.

Het degradatiebeleid bepaalt welke gebeurtenissen je als exceptions afhandelt en welke je als waarschuwingen verwerkt. NextPDF\Core\Config gebruikt standaard DegradationPolicy::Balanced, dat waarschuwt en doorgaat bij begrensde degradatie maar werpt bij blokkerende impact. DegradationPolicy::Permissive werpt nooit en verzamelt alles in het waarschuwingskanaal. DegradationPolicy::Strict werpt bij elke conformiteitsrisico-, semantisch-verlies- of blokkerende impact. Kies eerst het beleid en schrijf daarna herstel voor de foutvormen die dat beleid oplevert.

De onderstaande herstelcode gebruikt deze geverifieerde members:

  • 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): self en de standaard van degradationPolicy van DegradationPolicy::Balanced.
  • NextPDF\Contracts\DegradationPolicyStrict, Balanced, Permissive.
  • NextPDF\Exception\NextPdfException (abstracte basis), NextPDF\Exception\FontNotFoundException, NextPDF\Exception\HtmlParsingException, NextPDF\Exception\CssResolutionBudgetExceededException, NextPDF\Exception\WriterException, NextPDF\Exception\PageLayoutException.
  • NextPDF\Support\DegradedException (met capability en policy), NextPDF\Support\Capability (id, status, reason, isDegraded()), NextPDF\Support\Warning, NextPDF\Support\WarningSeverity.

De kleinste bruikbare herstelroutine vangt een fout door een ontbrekend lettertype op, valt terug op een gegarandeerd beschikbaar lettertype en gaat verder. Dit fragment laat de bredere afhandeling uit het productievoorbeeld weg. Voor een volledige handler met logging en de grens rond DegradedException lees je het productievoorbeeld hieronder.

<?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');

Het volledige voorbeeld brengt alle vier strategieën samen in één renderpijplijn: een lettertype-fallback, een renderer-fallback van het in-procespad naar Chrome, een herhaling met alternatieve HTML en herstel van een gedeeltelijk document op basis van getNumPages(). Het respecteert het uitvoerkanaal van de harness, vangt nooit een kale Exception op en laat geen catch-blok leeg.

<?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 blijft vrij voor de harness. Hersteldiagnostiek gaat naar STDERR, en het Portable Document Format (PDF)-bestand wordt alleen naar NEXTPDF_COOKBOOK_OUTPUT geschreven.

  • Orden de catch-blokken van specifiek naar algemeen. PHP matcht de eerste compatibele catch. Als je catch (NextPdfException $e) vóór catch (WriterException $e) plaatst, wordt het specifieke blok dode code, omdat WriterException NextPdfException uitbreidt.
  • DegradedException valt buiten de hiërarchie. Het breidt RuntimeException uit, niet NextPdfException. Een pijplijn die alleen NextPdfException opvangt, laat een afwijzing door het strikte beleid ongevangen doorgaan. Vang DegradedException (of een bredere RuntimeException) op als een niet-standaard degradatiebeleid actief is.
  • Een lettertype-fallback kan ook mislukken. Als je fallback-lettertype zelf niet is geregistreerd, werpt de tweede setFont() opnieuw. Gebruik een Base14-alias zoals helvetica, die de engine zonder bestandssysteemopzoeking oplost, of registreer een meegeleverd lettertype via addFontDirectory() bij het opstarten, zodat de fallback gegarandeerd is.
  • getNumPages() telt de actieve, nog niet weggeschreven pagina mee. Het retourneert het aantal weggeschreven pagina’s plus één wanneer er op dat moment een pagina open is. Een „gedeeltelijke opslag” omvat de pagina die werd opgebouwd toen de fout optrad, wat doorgaans is wat je wilt. Als je alleen volledig voltooide pagina’s nodig hebt, laat de code dan ook op getPage() vertakken.
  • De Chrome-fallback verandert de getrouwheid, niet alleen de beschikbaarheid. De in-procespijplijn en de Chrome-bridge gebruiken verschillende lay-out-engines, dus een document dat terugvalt op Chrome kan er anders uitzien. Behandel de fallback als herstel, niet als transparante vervanging, en leg vast via welk pad de uitvoer is voortgebracht.
  • Een herhaling moet bewezen goede invoer gebruiken. De herhaling met vereenvoudigde HTML helpt alleen wanneer de vereenvoudigde variant echt eenvoudiger is: minder geneste selectors, geen :has()-ketens die het resolutiebudget uitputten. Opnieuw proberen met dezelfde invoer die al is mislukt, leidt tot dezelfde exception.
  • Inspecteer waarschuwingen na een schone run. Een render die terugkeert zonder te werpen, kan toch zijn gedegradeerd. Controleer hasDegradedParity() en lees getWarnings() voordat je de uitvoer als pixelgetrouw behandelt; onder DegradationPolicy::Permissive is elke degradatie een waarschuwing, nooit een exception.
  • Herstel voegt alleen kosten toe op het foutpad. NextPDF werpt bij uitzonderlijke toestanden, dus een schone render betaalt niets voor de omringende try/catch.
  • Een renderer-fallback voert de render opnieuw uit. De in-procespoging wordt weggegooid en de Chrome-poging begint opnieuw, dus een fallback-render kost in het slechtste geval beide rendertijden plus de roundtrip tussen processen naar Chrome. Houd daar rekening mee wanneer je request-time-outs instelt.
  • Een herhaling met alternatieve HTML parset een tweede document. Houd de vereenvoudigde variant klein, zodat de herhaling goedkoop blijft ten opzichte van de primaire poging.
  • Een gedeeltelijke opslag serialiseert de al opgebouwde pagina’s. De kosten schalen met het aantal behouden pagina’s, niet met het werk dat is mislukt.
  • Toon eindgebruikers geen ruwe exception-berichten of bestandssysteempaden. Een FontNotFoundException-bericht bevat de doorzochte mappen en een WriterException bevat het uitvoerpad; beide lekken de serverstructuur. Log de gestructureerde context aan serverzijde en retourneer een generiek bericht naar de aanroeper.
  • Behandel HTML bij elke nieuwe poging als niet-vertrouwde invoer. De fallback en de herhaling met vereenvoudigde HTML passeren allebei dezelfde invoergrens; de in-procespijplijn en de Chrome-bridge passen elk hun eigen HTML-beveiligingsbeleid toe, en een herhaling versoepelt die validatie niet. Ga er niet van uit dat een „vereenvoudigde” variant veiliger is omdat je die zelf hebt opgesteld.
  • Een gedeeltelijke opslag schrijft nog steeds een bestand. Pas voor gedeeltelijke uitvoer dezelfde padvalidatie, machtigingen en regels voor opslaglocaties toe als voor volledige uitvoer. Document::save() weigert stream-wrappers en null-bytes en resolveert de bovenliggende map om path traversal te blokkeren, maar de bestemming die je doorgeeft, blijft jouw verantwoordelijkheid.

Deze pagina maakt geen normatieve claim over standaarden. Ze stelt de openbare exception- en documentinspectie-API’s van NextPDF samen tot een besturingsstroom voor herstel; ze bevestigt geen gedrag dat is gedefinieerd door ISO 32000-2 of een andere standaard, dus ze bevat geen citations:-blok.

Deze pagina wordt geverifieerd met het semantische reproduceerbaarheidsprofiel. Het herstelde document bevat een trailer /ID en een wijzigingsdatum die bij elke opslag opnieuw worden gegenereerd, dus byte-identiteit is niet haalbaar. De vergelijking op basis van de structurele abstract syntax tree (AST), aangevuld met alleen metadata, is stabiel over runs heen.