Lewati ke konten

Menerapkan strategi khusus untuk pemulihan kesalahan dan percobaan ulang

Layanan dokumen produksi melakukan lebih dari sekadar menangkap dan mencatat eksepsi. Layanan tersebut menentukan apa yang harus dilakukan berikutnya: melanjutkan dengan keluaran yang didegradasi, beralih ke jalur perenderan kedua, mencoba lagi dengan masukan yang dapat diterima mesin, atau mengirimkan halaman yang sudah dibangun sebelum kegagalan terjadi. Resep ini menunjukkan empat strategi pemulihan yang dibangun di atas hierarki eksepsi NextPDF dan metode inspeksi status dokumen:

  • Degradasi yang anggun saat kegagalan fon — tangkap NextPDF\Exception\FontNotFoundException, beralih ke jenis huruf yang dijamin tersedia, dan teruskan membangun dokumen.
  • Perender cadangan — ketika jalur dalam proses Document::writeHtml() menolak masukan, coba lagi melalui Document::writeHtmlChrome(), yaitu jembatan Chrome nextpdf/artisan.
  • Coba lagi dengan HTML alternatif — ketika NextPDF\Exception\HtmlParsingException atau NextPDF\Exception\CssResolutionBudgetExceededException terjadi, coba lagi dengan varian HTML yang disederhanakan dan diketahui andal.
  • Pemulihan dokumen parsial — baca Document::getNumPages() setelah kegagalan dan simpan apa yang sudah dibangun alih-alih membuangnya.

Anda sudah tahu cara menangkap eksepsi pada tingkat yang tepat. Halaman pendamping Tangani kesalahan dengan hierarki eksepsi NextPDF membahas hierarki itu sendiri. Halaman ini menunjukkan apa yang perlu Anda lakukan setelah eksepsi tertangkap.

Resep ini ditujukan untuk edisi inti perangkat lunak sumber terbuka (OSS). Setiap API yang disebutkan di sini berada di nextpdf/core. Satu-satunya dependensi opsional adalah nextpdf/artisan untuk cadangan Chrome.

Terminal window
composer require nextpdf/core:^3

Strategi perender cadangan juga menggunakan jembatan Chrome:

Terminal window
composer require nextpdf/artisan

Ketika nextpdf/artisan tidak ada, Document::writeHtmlChrome() melempar NextPDF\Exception\PageLayoutException alih-alih merender. Strategi cadangan di bawah ini memperlakukan jembatan yang tidak tersedia sebagai kasus lain yang dapat dipulihkan.

Pemulihan bergantung pada dua fakta tentang NextPDF, dan keduanya diverifikasi terhadap kode sumber.

Hierarki eksepsi memberi tahu Anda apa yang dapat dipulihkan. Setiap eksepsi domain memperluas kelas dasar abstrak NextPDF\Exception\NextPdfException, yang memperluas RuntimeException dan mengimplementasikan NextPDF\Contracts\ContextAwareExceptionInterface. Tangkap subtipe tertentu untuk memilih jalur pemulihan bagi kegagalan tersebut:

  • FontNotFoundException membawa getFontName(), getSearchPaths(), dan wasFallbackAttempted() — cukup untuk mencoba lagi dengan jenis huruf yang berbeda.
  • HtmlParsingException membawa getRule(), getPosition(), dan getHtmlSnippet() — cukup untuk memutuskan apakah percobaan ulang yang disederhanakan layak dicoba.
  • CssResolutionBudgetExceededException membawa getVisits() dan getBudget() — sinyal bahwa lembar gaya yang dipangkas mungkin dapat mengatasi pemilih yang patologis.
  • Satu batasan penting: NextPDF\Support\DegradedException memperluas RuntimeException secara langsung, bukan NextPdfException. Karena itu, catch (NextPdfException $e) tidak menangkap penolakan kebijakan penurunan kualitas. Ketika NextPDF\Contracts\DegradationPolicy yang aktif adalah Strict atau Balanced, tangkap DegradedException secara eksplisit untuk memulihkannya.

Dokumen dapat diinspeksi saat Anda membangunnya. Sebuah Document mengekspos status pembangunannya melalui pengakses hanya-baca. getNumPages() mengembalikan jumlah total halaman, termasuk halaman aktif yang belum di-flush, dan getPage() mengembalikan indeks berbasis nol dari halaman saat ini. Setelah kegagalan di tengah pembangunan, baca getNumPages() untuk mengetahui apakah ada halaman lengkap, lalu panggil save() atau getPdfData() untuk mengeluarkannya. Mesin juga mencatat peristiwa penurunan kualitas yang tidak fatal: getWarnings() mengembalikan sebuah list<NextPDF\Support\Warning>, hasWarnings() melaporkan apakah ada yang terkumpul, dan hasDegradedParity() melaporkan apakah kesetiaan keluaran terpengaruh. Metode-metode ini memungkinkan rutin pemulihan membedakan “berhasil bersih” dari “berhasil dengan kesetiaan yang berkurang” tanpa mengurai eksepsi.

Kebijakan penurunan kualitas menentukan peristiwa mana yang Anda tangani sebagai eksepsi dan mana yang Anda tangani sebagai peringatan. NextPDF\Core\Config secara default menggunakan DegradationPolicy::Balanced, yang memberi peringatan dan melanjutkan pada penurunan kualitas terbatas, tetapi melempar pada dampak yang menghalangi. DegradationPolicy::Permissive tidak pernah melempar dan mengumpulkan semuanya di saluran peringatan. DegradationPolicy::Strict melempar pada setiap risiko kepatuhan, kehilangan semantik, atau dampak yang menghalangi. Pilih kebijakan terlebih dahulu, lalu tulis pemulihan untuk bentuk kegagalan yang dihasilkan oleh kebijakan itu.

Kode pemulihan di bawah ini menggunakan anggota yang telah diverifikasi berikut:

  • 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 dan default degradationPolicy adalah DegradationPolicy::Balanced.
  • NextPDF\Contracts\DegradationPolicyStrict, Balanced, Permissive.
  • NextPDF\Exception\NextPdfException (basis abstrak), NextPDF\Exception\FontNotFoundException, NextPDF\Exception\HtmlParsingException, NextPDF\Exception\CssResolutionBudgetExceededException, NextPDF\Exception\WriterException, NextPDF\Exception\PageLayoutException.
  • NextPDF\Support\DegradedException (membawa capability dan policy), NextPDF\Support\Capability (id, status, reason, isDegraded()), NextPDF\Support\Warning, NextPDF\Support\WarningSeverity.

Pemulihan paling kecil yang berguna adalah menangkap kegagalan akibat fon yang hilang, beralih ke jenis huruf yang dijamin tersedia, dan melanjutkan. Cuplikan ini melewatkan penanganan yang lebih luas dari contoh produksi. Untuk penangan lengkap dengan pencatatan dan batas DegradedException, baca contoh produksi di bawah ini.

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

Contoh lengkap merangkai keempat strategi dalam satu alur perenderan: cadangan fon, cadangan perender dari jalur dalam proses ke Chrome, percobaan ulang HTML alternatif, dan pemulihan dokumen parsial yang digerakkan oleh getNumPages(). Contoh ini mematuhi saluran keluaran harness dan tidak pernah menangkap Exception mentah atau membiarkan blok catch kosong.

<?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 tetap bebas untuk harness. Diagnostik pemulihan dikirim ke STDERR, dan berkas Portable Document Format (PDF) hanya ditulis ke NEXTPDF_COOKBOOK_OUTPUT.

  • Susun blok catch dari yang spesifik ke yang umum. PHP mencocokkan catch kompatibel pertama. Menempatkan catch (NextPdfException $e) sebelum catch (WriterException $e) membuat blok yang spesifik menjadi kode mati, karena WriterException memperluas NextPdfException.
  • DegradedException berada di luar hierarki. Eksepsi ini memperluas RuntimeException, bukan NextPdfException. Alur yang hanya menangkap NextPdfException membiarkan penolakan kebijakan ketat menyebar tanpa tertangkap. Tangkap DegradedException (atau RuntimeException yang lebih luas) ketika kebijakan penurunan kualitas non-default sedang aktif.
  • Cadangan fon juga bisa gagal. Jika fon cadangan Anda sendiri tidak terdaftar, setFont() kedua akan melempar lagi. Gunakan alias Base14 seperti helvetica, yang diselesaikan mesin tanpa pencarian sistem berkas, atau daftarkan fon yang dibundel melalui addFontDirectory() saat startup agar cadangan terjamin.
  • getNumPages() menghitung halaman aktif yang belum di-flush. Metode ini mengembalikan jumlah halaman yang sudah di-flush ditambah satu ketika sebuah halaman sedang terbuka. “Penyimpanan parsial” menyertakan halaman yang sedang dibangun ketika kegagalan terjadi, yang biasanya memang yang Anda inginkan. Jika Anda hanya membutuhkan halaman yang sepenuhnya selesai, cabangkan juga pada getPage().
  • Cadangan Chrome mengubah kesetiaan, bukan hanya ketersediaan. Jalur dalam proses dan jembatan Chrome menggunakan mesin tata letak yang berbeda, sehingga dokumen yang beralih ke Chrome dapat terlihat berbeda. Perlakukan cadangan sebagai pemulihan, bukan sebagai pengganti yang transparan, dan catat jalur mana yang menghasilkan keluaran.
  • Percobaan ulang harus menggunakan masukan yang diketahui andal. Percobaan ulang HTML yang disederhanakan hanya membantu ketika varian yang disederhanakan benar-benar lebih sederhana: lebih sedikit pemilih bersarang, tanpa rantai :has() yang menghabiskan anggaran resolusi. Mencoba lagi dengan masukan sama yang sudah gagal akan menghasilkan eksepsi yang sama.
  • Periksa peringatan setelah proses yang bersih. Perenderan yang kembali tanpa melempar pun masih bisa mengalami penurunan kualitas. Periksa hasDegradedParity() dan baca getWarnings() sebelum Anda memperlakukan keluaran sebagai setia per piksel; di bawah DegradationPolicy::Permissive, setiap penurunan kualitas menjadi peringatan, bukan eksepsi.
  • Pemulihan hanya menambah biaya pada jalur kegagalan. NextPDF melempar pada keadaan luar biasa, sehingga perenderan yang bersih tidak membayar apa pun untuk try/catch di sekitarnya.
  • Perender cadangan menjalankan ulang perenderan. Upaya dalam proses dibuang dan upaya Chrome dimulai dari awal, sehingga perenderan cadangan, dalam kasus terburuk, menghabiskan waktu kedua proses perenderan ditambah perjalanan bolak-balik antarproses ke Chrome. Anggarkan hal itu ketika Anda menetapkan batas waktu permintaan.
  • Percobaan ulang HTML alternatif mengurai dokumen kedua. Jaga agar varian yang disederhanakan tetap kecil sehingga percobaan ulang murah relatif terhadap upaya utama.
  • Penyimpanan parsial menserialkan halaman yang sudah dibangun. Biayanya meningkat seiring jumlah halaman yang bertahan, bukan pekerjaan yang gagal.
  • Jangan tampilkan pesan eksepsi mentah atau jalur sistem berkas kepada pengguna akhir. Pesan FontNotFoundException mencakup direktori yang dicari dan WriterException mencakup jalur keluaran; keduanya membocorkan tata letak server. Catat konteks terstruktur di sisi server dan kembalikan pesan umum kepada pemanggil.
  • Perlakukan HTML yang dicoba ulang sebagai masukan tidak tepercaya pada setiap upaya. Cadangan dan percobaan ulang HTML yang disederhanakan sama-sama mengalir melalui batas masukan yang sama; jalur dalam proses dan jembatan Chrome masing-masing menerapkan kebijakan keamanan HTML-nya sendiri, dan percobaan ulang tidak melonggarkan validasi tersebut. Jangan berasumsi bahwa varian “disederhanakan” lebih aman hanya karena Anda yang menulisnya.
  • Penyimpanan parsial tetap menulis sebuah berkas. Terapkan validasi jalur, izin, dan aturan lokasi penyimpanan yang sama pada keluaran parsial seperti yang Anda terapkan pada keluaran lengkap. Document::save() menolak pembungkus aliran dan bita nol serta menyelesaikan direktori induk untuk memblokir penjelajahan jalur, tetapi tujuan yang Anda berikan tetap menjadi tanggung jawab Anda.

Resep ini tidak membuat klaim standar normatif. Resep ini menyusun API eksepsi dan inspeksi dokumen publik NextPDF menjadi alur kendali pemulihan; resep ini tidak menegaskan perilaku yang didefinisikan oleh ISO 32000-2 atau standar lainnya, sehingga tidak menyertakan blok citations:.

Halaman ini diverifikasi dengan profil reproduktibilitas semantik. Dokumen yang dipulihkan membawa trailer /ID dan tanggal modifikasi yang diregenerasi pada setiap penyimpanan, sehingga identitas bita tidak dapat dicapai. Perbandingan struktural pohon sintaks abstrak (AST), ditambah pemeriksaan metadata saja, tetap stabil di seluruh proses.