Przejdź do głównej zawartości

Własne strategie odzyskiwania po błędach i ponawiania prób

Produkcyjna usługa generowania dokumentów nie kończy na przechwyceniu i zarejestrowaniu wyjątku. Musi zdecydować, co dalej: kontynuować z obniżoną jakością wyniku, przełączyć się na alternatywną ścieżkę renderowania, ponowić próbę z danymi wejściowymi akceptowanymi przez silnik albo dostarczyć strony zbudowane przed wystąpieniem błędu. Ten przepis przedstawia cztery strategie odzyskiwania oparte na hierarchii wyjątków NextPDF oraz metodach badania stanu dokumentu:

  • Łagodna degradacja przy błędzie czcionki — przechwyć NextPDF\Exception\FontNotFoundException, przełącz się na gwarantowany krój i kontynuuj budowanie dokumentu.
  • Zapasowy renderer — gdy ścieżka Document::writeHtml() działająca w procesie odrzuci dane wejściowe, ponów próbę przez Document::writeHtmlChrome(), czyli most nextpdf/artisan do Chrome.
  • Ponowienie próby z alternatywnym kodem HTML — gdy wystąpi NextPDF\Exception\HtmlParsingException lub NextPDF\Exception\CssResolutionBudgetExceededException, ponów próbę z uproszczonym, sprawdzonym wariantem HTML.
  • Odzyskiwanie częściowego dokumentu — odczytaj Document::getNumPages() po błędzie i zapisz to, co zostało już zbudowane, zamiast je odrzucać.

Wiesz już, jak przechwytywać wyjątki na odpowiednim poziomie. Powiązana strona Obsługa błędów z hierarchią wyjątków NextPDF opisuje samą hierarchię. Ta strona pokazuje, co zrobić po przechwyceniu.

Ten przepis dotyczy edycji core open source software (OSS). Wszystkie elementy application programming interface (API) wymienione tutaj znajdują się w nextpdf/core. Jedyną opcjonalną zależnością jest nextpdf/artisan na potrzeby rozwiązania zapasowego opartego na Chrome.

Okno terminala
composer require nextpdf/core:^3

Strategia zapasowego renderera dodatkowo korzysta z mostu do Chrome:

Okno terminala
composer require nextpdf/artisan

Gdy brakuje nextpdf/artisan, Document::writeHtmlChrome() zamiast renderować, rzuca NextPDF\Exception\PageLayoutException. Strategia zapasowa przedstawiona poniżej traktuje brakujący most jako kolejny przypadek możliwy do obsłużenia w ramach odzyskiwania.

Odzyskiwanie zależy od dwóch faktów dotyczących NextPDF; oba zweryfikowano względem kodu źródłowego.

Hierarchia wyjątków wskazuje, które błędy można obsłużyć odzyskiwaniem. Każdy wyjątek domenowy rozszerza abstrakcyjną klasę bazową NextPDF\Exception\NextPdfException, która rozszerza RuntimeException i implementuje NextPDF\Contracts\ContextAwareExceptionInterface. Przechwyć konkretny podtyp, aby wybrać ścieżkę odzyskiwania dla danego błędu:

  • FontNotFoundException udostępnia getFontName(), getSearchPaths() oraz wasFallbackAttempted() — wystarczająco dużo informacji, aby ponowić próbę z innym krojem.
  • HtmlParsingException udostępnia getRule(), getPosition() oraz getHtmlSnippet() — wystarczająco dużo informacji, aby zdecydować, czy warto podjąć uproszczone ponowienie próby.
  • CssResolutionBudgetExceededException udostępnia getVisits() oraz getBudget() — to sygnał, że okrojony arkusz stylów może rozwiązać problem z patologicznym selektorem.
  • Istotna granica: NextPDF\Support\DegradedException rozszerza RuntimeException bezpośrednio, a nie NextPdfException. Dlatego catch (NextPdfException $e) nie przechwytuje odrzucenia wynikającego z zasad degradacji. Gdy aktywną NextPDF\Contracts\DegradationPolicy jest Strict lub Balanced, jawnie przechwyć DegradedException, aby móc odzyskać działanie po takim odrzuceniu.

Dokument można badać podczas budowania. Klasa Document udostępnia stan budowania przez akcesory tylko do odczytu. getNumPages() zwraca całkowitą liczbę stron, łącznie z aktywną, jeszcze niezapisaną stroną, a getPage() zwraca liczony od zera indeks bieżącej strony. Po błędzie podczas budowania odczytaj getNumPages(), aby dowiedzieć się, czy powstały już jakiekolwiek kompletne strony, a następnie wywołaj save() lub getPdfData(), aby je wyemitować. Silnik rejestruje również niekrytyczne zdarzenia degradacji: getWarnings() zwraca list<NextPDF\Support\Warning>, hasWarnings() informuje, czy zebrano jakiekolwiek ostrzeżenia, a hasDegradedParity() informuje, czy degradacja wpłynęła na wierność wyniku. Te metody pozwalają procedurze odzyskiwania odróżnić „zakończono powodzeniem bez zastrzeżeń” od „zakończono powodzeniem z obniżoną wiernością” bez analizowania wyjątku.

Zasady degradacji decydują, które zdarzenia są obsługiwane jako wyjątki, a które jako ostrzeżenia. NextPDF\Core\Config domyślnie używa DegradationPolicy::Balanced, która ostrzega i kontynuuje przy ograniczonej degradacji, ale rzuca wyjątek przy wpływie blokującym. DegradationPolicy::Permissive nigdy nie rzuca wyjątku i zbiera wszystko w kanale ostrzeżeń. DegradationPolicy::Strict rzuca wyjątek przy każdym ryzyku zgodności, utracie znaczenia lub wpływie blokującym. Najpierw wybierz zasady, a następnie napisz odzyskiwanie dla rodzajów błędów, które te zasady wytwarzają.

Poniższy kod odzyskiwania korzysta z następujących zweryfikowanych składowych:

  • 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 oraz domyślna wartość degradationPolicy równa DegradationPolicy::Balanced.
  • NextPDF\Contracts\DegradationPolicyStrict, Balanced, Permissive.
  • NextPDF\Exception\NextPdfException (abstrakcyjna klasa bazowa), NextPDF\Exception\FontNotFoundException, NextPDF\Exception\HtmlParsingException, NextPDF\Exception\CssResolutionBudgetExceededException, NextPDF\Exception\WriterException, NextPDF\Exception\PageLayoutException.
  • NextPDF\Support\DegradedException (zawierająca capability i policy), NextPDF\Support\Capability (id, status, reason, isDegraded()), NextPDF\Support\Warning, NextPDF\Support\WarningSeverity.

Najprostszy użyteczny wariant odzyskiwania przechwytuje błąd brakującej czcionki, przełącza dokument na gwarantowany krój i kontynuuje działanie. Fragment pomija szerszą obsługę z przykładu produkcyjnego. Pełną obsługę z rejestrowaniem oraz granicą DegradedException znajdziesz w przykładzie produkcyjnym poniżej.

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

Pełny przykład łączy wszystkie cztery strategie w jednym potoku renderowania: rozwiązanie zapasowe dla czcionki, zapasowy renderer przełączający ze ścieżki działającej w procesie na Chrome, ponowienie próby z alternatywnym kodem HTML oraz odzyskiwanie częściowego dokumentu sterowane przez getNumPages(). Uwzględnia kanał wyjściowy szkieletu testowego i nigdy nie przechwytuje gołego Exception ani nie zostawia pustego bloku 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 pozostaje dostępny dla szkieletu testowego. Diagnostyka odzyskiwania trafia do STDERR, a plik w formacie Portable Document Format (PDF) jest zapisywany wyłącznie do NEXTPDF_COOKBOOK_OUTPUT.

  • Porządkuj bloki catch od szczegółowych do ogólnych. PHP dopasowuje pierwszy zgodny catch. Umieszczenie catch (NextPdfException $e) przed catch (WriterException $e) zamienia ten szczegółowy blok w martwy kod, ponieważ WriterException rozszerza NextPdfException.
  • DegradedException znajduje się poza hierarchią. Rozszerza RuntimeException, a nie NextPdfException. Potok przechwytujący tylko NextPdfException sprawia, że odrzucenie wynikające z zasad Strict propaguje się bez przechwycenia. Przechwyć DegradedException (lub szerszy RuntimeException), gdy aktywne są zasady degradacji inne niż domyślne.
  • Rozwiązanie zapasowe dla czcionki też może zawieść. Jeśli sam krój zapasowy nie jest zarejestrowany, drugie wywołanie setFont() ponownie rzuca wyjątek. Użyj aliasu Base14, takiego jak helvetica, który silnik rozwiązuje bez odwołania do systemu plików, albo zarejestruj dołączony krój przez addFontDirectory() przy starcie, aby rozwiązanie zapasowe było gwarantowane.
  • getNumPages() liczy aktywną, jeszcze niezapisaną stronę. Zwraca liczbę zapisanych stron powiększoną o jeden, gdy jakaś strona jest aktualnie otwarta. „Częściowy zapis” obejmuje stronę, która była budowana w chwili wystąpienia błędu, co zwykle jest pożądane. Jeśli potrzebujesz wyłącznie w pełni ukończonych stron, dodaj też rozgałęzienie według getPage().
  • Rozwiązanie zapasowe oparte na Chrome zmienia wierność, a nie tylko dostępność. Potok działający w procesie oraz most do Chrome korzystają z różnych silników układu, więc dokument przełączony na Chrome może wyglądać inaczej. Traktuj rozwiązanie zapasowe jako odzyskiwanie, a nie niezauważalne zastępstwo, i odnotuj, która ścieżka wytworzyła wynik.
  • Ponowienie próby musi używać sprawdzonych danych wejściowych. Ponowienie próby z uproszczonym kodem HTML pomaga tylko wtedy, gdy uproszczony wariant jest naprawdę prostszy: mniej zagnieżdżonych selektorów, brak łańcuchów z :has(), które wyczerpują budżet rozwiązywania. Ponawianie próby z tymi samymi danymi wejściowymi, które już zawiodły, tylko wraca do tego samego wyjątku.
  • Sprawdzaj ostrzeżenia po bezbłędnym przebiegu. Renderowanie, które kończy się bez rzucenia wyjątku, i tak mogło ulec degradacji. Sprawdź hasDegradedParity() i odczytaj getWarnings(), zanim potraktujesz wynik jako wierny co do piksela; przy DegradationPolicy::Permissive każda degradacja jest ostrzeżeniem, nigdy wyjątkiem.
  • Odzyskiwanie dodaje koszt wyłącznie na ścieżce błędu. NextPDF rzuca wyjątek w stanach wyjątkowych, więc bezbłędne renderowanie nie ponosi kosztu otaczającego try/catch.
  • Rozwiązanie zapasowe renderera uruchamia renderowanie ponownie. Próba w procesie jest odrzucana, a próba w Chrome zaczyna się od nowa, więc renderowanie zapasowe kosztuje w najgorszym przypadku oba czasy renderowania plus narzut komunikacji międzyprocesowej z Chrome. Uwzględnij to w budżecie przy ustawianiu limitów czasu żądań.
  • Ponowienie próby z alternatywnym kodem HTML parsuje drugi dokument. Utrzymuj uproszczony wariant niewielkim, aby ponowienie próby było tanie względem próby podstawowej.
  • Częściowy zapis serializuje strony, które zostały już zbudowane. Jego koszt skaluje się wraz z liczbą zachowanych stron, a nie z pracą, która zawiodła.
  • Nie pokazuj użytkownikom końcowym surowych komunikatów wyjątków ani ścieżek w systemie plików. Komunikat FontNotFoundException zawiera przeszukiwane katalogi, a WriterException zawiera ścieżkę wyjściową; oba ujawniają układ serwera. Rejestruj strukturalny kontekst po stronie serwera i zwracaj wywołującemu ogólny komunikat.
  • Traktuj ponawiany kod HTML jako niezaufane dane wejściowe przy każdej próbie. Zarówno rozwiązanie zapasowe, jak i ponowienie próby z uproszczonym kodem HTML przekraczają tę samą granicę wejściową; potok działający w procesie oraz most do Chrome stosują własne zasady bezpieczeństwa HTML, a ponowienie próby nie osłabia tej walidacji. Nie zakładaj, że „uproszczony” wariant jest bezpieczniejszy tylko dlatego, że powstał we własnym kodzie.
  • Częściowy zapis i tak tworzy plik. Do częściowego wyniku stosuj tę samą walidację ścieżki, uprawnienia i zasady wyboru lokalizacji przechowywania, które stosujesz do pełnego wyniku. Document::save() odrzuca opakowania strumieni i bajty null oraz rozwiązuje katalog nadrzędny, aby zablokować przechodzenie po ścieżkach, ale za przekazywane miejsce docelowe odpowiada wywołujący.

Ten przepis nie deklaruje żadnej normatywnej zgodności ze standardami. Łączy publiczne API wyjątków i badania dokumentów NextPDF w przepływ sterowania odzyskiwaniem; nie deklaruje zachowania zdefiniowanego przez ISO 32000-2 ani żaden inny standard, więc nie zawiera bloku citations:.

Ta strona jest weryfikowana przy użyciu semantycznego profilu odtwarzalności. Odzyskany dokument zawiera w trailerze /ID oraz datę modyfikacji, które są generowane na nowo przy każdym zapisie, więc identyczność bajtowa nie jest osiągalna. Porównanie strukturalnego drzewa składni abstrakcyjnej (AST) wraz z samymi metadanymi jest stabilne między uruchomieniami.