Salta ai contenuti

Implementare strategie personalizzate per il ripristino dagli errori e i nuovi tentativi

Un servizio di generazione documenti in produzione non si limita a intercettare un’eccezione e registrarla nei log. Stabilisce il passo successivo: proseguire con un risultato degradato, passare a un secondo percorso di rendering, riprovare con un input accettato dal motore oppure consegnare le pagine già costruite prima dell’errore. Questa ricetta illustra quattro strategie di ripristino basate sulla gerarchia di eccezioni di NextPDF e sui metodi di ispezione dello stato del documento:

  • Degradazione controllata in caso di errore di un font — intercettare NextPDF\Exception\FontNotFoundException, ripiegare su un carattere garantito e proseguire la costruzione del documento.
  • Renderer di ripiego — quando il percorso in-process Document::writeHtml() rifiuta l’input, riprovare tramite Document::writeHtmlChrome(), il bridge Chrome di nextpdf/artisan.
  • Nuovo tentativo con HTML alternativo — quando si verifica NextPDF\Exception\HtmlParsingException oppure NextPDF\Exception\CssResolutionBudgetExceededException, riprovare con una variante HTML semplificata e sicuramente affidabile.
  • Ripristino di un documento parziale — leggere Document::getNumPages() dopo un errore e salvare ciò che è già stato costruito invece di scartarlo.

Si presuppone che l’intercettazione con la giusta granularità sia già nota. La pagina complementare Gestire gli errori con la gerarchia di eccezioni di NextPDF tratta la gerarchia in sé. Questa pagina tratta ciò che avviene dopo l’intercettazione.

Questa ricetta è destinata all’edizione core OSS. Ogni API citata qui risiede in nextpdf/core. L’unica dipendenza facoltativa è nextpdf/artisan per il ripiego su Chrome.

Terminal window
composer require nextpdf/core:^3

La strategia del renderer di ripiego usa inoltre il bridge Chrome:

Terminal window
composer require nextpdf/artisan

Quando nextpdf/artisan è assente, Document::writeHtmlChrome() genera NextPDF\Exception\PageLayoutException anziché eseguire il rendering; di conseguenza, la strategia di ripiego descritta più avanti considera anche l’assenza del bridge come un caso recuperabile.

Il ripristino si basa su due fatti relativi a NextPDF, entrambi verificati sul codice sorgente.

La gerarchia di eccezioni indica ciò che è recuperabile. Ogni eccezione di dominio estende la classe base astratta NextPDF\Exception\NextPdfException, che a sua volta estende RuntimeException e implementa NextPDF\Contracts\ContextAwareExceptionInterface. Intercettare un sottotipo specifico permette di scegliere un percorso di ripristino calibrato sull’errore:

  • FontNotFoundException espone getFontName(), getSearchPaths() e wasFallbackAttempted() — sufficienti per riprovare con un carattere diverso.
  • HtmlParsingException espone getRule(), getPosition() e getHtmlSnippet() — sufficienti per decidere se valga la pena effettuare un nuovo tentativo semplificato.
  • CssResolutionBudgetExceededException espone getVisits() e getBudget() — il segnale di un selettore patologico che un foglio di stile ridotto all’essenziale può risolvere.
  • Un confine importante: NextPDF\Support\DegradedException estende RuntimeException direttamente, non NextPdfException. Pertanto catch (NextPdfException $e) non intercetta un rifiuto della policy di degradazione. Quando la NextPDF\Contracts\DegradationPolicy attiva è Strict o Balanced, intercettare esplicitamente DegradedException per gestirne il ripristino.

Il documento è ispezionabile mentre lo si costruisce. Un Document espone il proprio stato di costruzione tramite accessor di sola lettura. getNumPages() restituisce il numero totale di pagine, inclusa la pagina attiva non ancora scaricata, mentre getPage() restituisce l’indice in base zero della pagina corrente. Dopo un errore a metà costruzione, leggere getNumPages() per sapere se esistono pagine complete, quindi chiamare save() o getPdfData() per emetterle. Il motore registra inoltre gli eventi di degradazione non fatali: getWarnings() restituisce una list<NextPDF\Support\Warning>, hasWarnings() segnala se ne sono stati raccolti e hasDegradedParity() segnala se la fedeltà dell’output è stata compromessa. Questi metodi consentono a una routine di ripristino di distinguere tra «riuscito in modo pulito» e «riuscito con fedeltà ridotta» senza analizzare alcuna eccezione.

La policy di degradazione determina quali eventi vengono gestiti come eccezioni anziché come avvisi. Il valore predefinito di NextPDF\Core\Config è DegradationPolicy::Balanced, che emette un avviso e prosegue in caso di degradazione limitata, ma genera un’eccezione in caso di impatto bloccante. DegradationPolicy::Permissive non genera mai eccezioni e raccoglie tutto nel canale degli avvisi. DegradationPolicy::Strict genera un’eccezione in presenza di qualsiasi rischio di conformità, perdita semantica o impatto bloccante. Scegliere prima la policy, quindi scrivere il ripristino in base alle forme di errore prodotte da tale policy.

Il codice di ripristino riportato di seguito usa questi membri verificati:

  • 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 e il valore predefinito degradationPolicy pari a DegradationPolicy::Balanced.
  • NextPDF\Contracts\DegradationPolicyStrict, Balanced, Permissive.
  • NextPDF\Exception\NextPdfException (classe base astratta), NextPDF\Exception\FontNotFoundException, NextPDF\Exception\HtmlParsingException, NextPDF\Exception\CssResolutionBudgetExceededException, NextPDF\Exception\WriterException, NextPDF\Exception\PageLayoutException.
  • NextPDF\Support\DegradedException (che trasporta capability e policy), NextPDF\Support\Capability (id, status, reason, isDegraded()), NextPDF\Support\Warning, NextPDF\Support\WarningSeverity.

Il ripristino utile minimo consiste nell’intercettare un errore di font mancante, ripiegare su un carattere garantito e proseguire. Questo frammento tralascia la gestione più ampia presente nell’esempio di produzione. Per un gestore completo con logging e il confine DegradedException, consultare l’esempio di produzione riportato più avanti.

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

L’esempio completo integra tutte e quattro le strategie in un’unica pipeline di rendering: un ripiego del font, il ripiego del renderer dal percorso in-process a Chrome, un nuovo tentativo con HTML alternativo e il ripristino di un documento parziale guidato da getNumPages(). Rispetta il canale di output dell’harness e non intercetta mai una Exception generica né lascia vuoto un blocco catch.

<?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 resta libero per l’harness. Le diagnostiche di ripristino vanno su STDERR e il PDF viene scritto soltanto in NEXTPDF_COOKBOOK_OUTPUT.

  • Ordinare i blocchi catch dal più specifico al più generale. PHP associa il primo catch compatibile. Collocare catch (NextPdfException $e) prima di catch (WriterException $e) rende il blocco specifico irraggiungibile, perché WriterException estende NextPdfException.
  • DegradedException si colloca al di fuori della gerarchia. Estende RuntimeException, non NextPdfException. Una pipeline che intercetta soltanto NextPdfException lascia propagare senza intercettarlo un rifiuto di policy stretta. Intercettare DegradedException (o una più ampia RuntimeException) quando è attiva una policy di degradazione diversa da quella predefinita.
  • Anche un ripiego del font può fallire. Se il carattere di ripiego non è a sua volta registrato, la seconda setFont() genera nuovamente un’eccezione. Usare un alias Base14 come helvetica, che il motore risolve senza una ricerca sul filesystem, oppure registrare un carattere incluso tramite addFontDirectory() all’avvio, in modo che il ripiego sia garantito.
  • getNumPages() conta anche la pagina attiva non ancora scaricata. Restituisce il numero di pagine scaricate più uno quando una pagina è attualmente aperta. Pertanto un «salvataggio parziale» include la pagina in costruzione al momento dell’errore, che di norma è ciò che si desidera. Se servono soltanto le pagine completate del tutto, applicare una diramazione anche in base a getPage().
  • Il ripiego su Chrome modifica la fedeltà, non solo la disponibilità. La pipeline in-process e il bridge Chrome usano motori di impaginazione diversi, quindi un documento che ricorre a Chrome può avere un aspetto differente. Trattare il ripiego come un ripristino, non come un sostituto trasparente, e registrare quale percorso ha prodotto l’output.
  • Un nuovo tentativo deve usare un input sicuramente affidabile. Il nuovo tentativo con HTML semplificato è utile solo quando la variante semplificata è davvero più semplice: meno selettori annidati, nessuna catena :has() che esaurisce il budget di risoluzione. Riprovare con lo stesso input che è già fallito porta in loop sulla medesima eccezione.
  • Ispezionare gli avvisi dopo un’esecuzione pulita. Un rendering che termina senza generare eccezioni può comunque essere risultato degradato. Verificare hasDegradedParity() e leggere getWarnings() prima di considerare l’output fedele al pixel; con DegradationPolicy::Permissive ogni degradazione è un avviso, mai un’eccezione.
  • Il ripristino aggiunge un costo soltanto nel percorso di errore. NextPDF genera eccezioni in stati eccezionali, quindi un rendering pulito non paga nulla per il try/catch circostante.
  • Un ripiego del renderer riesegue il rendering. Il tentativo in-process viene scartato e il tentativo Chrome riparte da zero, quindi un rendering di ripiego costa, nel caso peggiore, entrambi i tempi di rendering più il round trip interprocesso verso Chrome. Tenerne conto nel budget quando si impostano i timeout delle richieste.
  • Un nuovo tentativo con HTML alternativo analizza un secondo documento. Mantenere ridotta la variante semplificata, in modo che il nuovo tentativo resti poco oneroso rispetto al tentativo primario.
  • Un salvataggio parziale serializza le pagine già costruite. Il suo costo cresce in proporzione al numero di pagine superstiti, non al lavoro fallito.
  • Non esporre agli utenti finali messaggi grezzi di eccezione o percorsi del filesystem. Il messaggio di una FontNotFoundException include le directory in cui è stata effettuata la ricerca e una WriterException include il percorso di output; entrambi divulgano la struttura del server. Registrare nei log il contesto strutturato lato server e restituire al chiamante un messaggio generico.
  • Trattare l’HTML del nuovo tentativo come input non attendibile in ogni tentativo. Sia il ripiego sia il nuovo tentativo con HTML semplificato attraversano lo stesso confine di input; la pipeline in-process e il bridge Chrome applicano ciascuno la propria policy di sicurezza HTML e un nuovo tentativo non allenta tale convalida. Non presumere che una variante «semplificata» sia più sicura solo perché è stata redatta internamente.
  • Un salvataggio parziale scrive comunque un file. Applicare a un output parziale le stesse regole di convalida del percorso, dei permessi e della posizione di archiviazione che si applicano a un output completo. Document::save() rifiuta gli stream wrapper e i null byte e risolve la directory padre per bloccare il path traversal, ma la destinazione passata resta responsabilità dell’utente.

Questa ricetta non formula alcuna affermazione normativa sugli standard. Combina le API pubbliche di NextPDF per le eccezioni e l’ispezione dei documenti in un flusso di controllo per il ripristino; non afferma alcun comportamento definito da ISO 32000-2 o da qualsiasi altro standard, quindi non richiede alcun blocco citations:.

È verificata con il profilo di riproducibilità semantico. Il documento ripristinato contiene un /ID nel trailer e una data di modifica che vengono rigenerati a ogni salvataggio, quindi l’identità a livello di byte non è raggiungibile. Il confronto basato sull’AST strutturale più i soli metadati è stabile tra le esecuzioni.