Bỏ qua để đến nội dung

Triển khai chiến lược khôi phục sau lỗi và thử lại tùy chỉnh

Một dịch vụ tài liệu trong môi trường thực tế không chỉ bắt và ghi nhật ký một ngoại lệ. Nó phải quyết định bước tiếp theo: tiếp tục với kết quả đã hạ cấp, chuyển sang một đường kết xuất thứ hai, thử lại với dữ liệu đầu vào mà engine chấp nhận, hoặc cung cấp các trang đã được dựng trước khi xảy ra lỗi. Công thức này trình bày bốn chiến lược khôi phục được xây dựng trên hệ thống phân cấp ngoại lệ của NextPDF và các phương thức kiểm tra trạng thái tài liệu:

  • Hạ cấp êm ái khi lỗi phông chữ — bắt NextPDF\Exception\FontNotFoundException, chuyển sang một phông chữ được bảo đảm khả dụng, rồi tiếp tục dựng tài liệu.
  • Bộ kết xuất dự phòng — khi đường Document::writeHtml() trong tiến trình từ chối dữ liệu đầu vào, hãy thử lại thông qua Document::writeHtmlChrome(), cầu nối Chrome nextpdf/artisan.
  • Thử lại với HTML thay thế — khi NextPDF\Exception\HtmlParsingException hoặc NextPDF\Exception\CssResolutionBudgetExceededException xảy ra, hãy thử lại với một biến thể HTML đã đơn giản hóa, chắc chắn hợp lệ.
  • Khôi phục tài liệu một phần — đọc Document::getNumPages() sau khi lỗi xảy ra, rồi lưu lại phần đã dựng được thay vì loại bỏ nó.

Bạn đã biết cách bắt lỗi ở đúng cấp độ. Trang đồng hành Xử lý lỗi với hệ thống phân cấp ngoại lệ của NextPDF giải thích chính hệ thống phân cấp đó. Trang này tập trung vào những việc bạn làm sau khi bắt lỗi.

Công thức này hướng đến phiên bản core của phần mềm mã nguồn mở (open source software, OSS). Mọi giao diện lập trình ứng dụng (application programming interface, API) được nêu ở đây đều thuộc nextpdf/core. Phụ thuộc tùy chọn duy nhất là nextpdf/artisan cho cơ chế dự phòng Chrome.

Terminal window
composer require nextpdf/core:^3

Chiến lược dùng bộ kết xuất dự phòng còn cần thêm cầu nối Chrome:

Terminal window
composer require nextpdf/artisan

Khi không có nextpdf/artisan, Document::writeHtmlChrome() sẽ ném ra NextPDF\Exception\PageLayoutException thay vì kết xuất. Chiến lược dự phòng dưới đây xem việc thiếu cầu nối là một trường hợp khác có thể khôi phục.

Việc khôi phục dựa trên hai điểm thực tế về NextPDF, cả hai đều đã được kiểm chứng từ mã nguồn.

Hệ thống phân cấp ngoại lệ cho biết điều gì có thể khôi phục. Mọi ngoại lệ miền đều kế thừa từ lớp cơ sở trừu tượng NextPDF\Exception\NextPdfException; lớp này kế thừa RuntimeException và triển khai NextPDF\Contracts\ContextAwareExceptionInterface. Hãy bắt một kiểu con cụ thể để chọn đường khôi phục phù hợp với lỗi đó:

  • FontNotFoundException mang theo getFontName(), getSearchPaths(), và wasFallbackAttempted() — đủ để thử lại với một phông chữ khác.
  • HtmlParsingException mang theo getRule(), getPosition(), và getHtmlSnippet() — đủ để quyết định liệu một lần thử lại đã đơn giản hóa có đáng thực hiện hay không.
  • CssResolutionBudgetExceededException mang theo getVisits()getBudget() — một tín hiệu cho thấy một bảng định kiểu được giản lược có thể xử lý bộ chọn có vấn đề.
  • Một ranh giới quan trọng: NextPDF\Support\DegradedException kế thừa RuntimeException một cách trực tiếp, chứ không phải NextPdfException. Vì vậy, catch (NextPdfException $e) không bắt được một lần từ chối do chính sách hạ cấp. Khi NextPDF\Contracts\DegradationPolicy hiện hoạt là Strict hoặc Balanced, hãy bắt DegradedException một cách tường minh để khôi phục từ nó.

Có thể kiểm tra tài liệu trong lúc dựng. Một Document cung cấp trạng thái dựng thông qua các bộ truy xuất chỉ đọc. getNumPages() trả về tổng số trang, bao gồm cả trang hiện hoạt chưa được ghi ra, và getPage() trả về chỉ mục tính từ 0 của trang hiện tại. Sau khi gặp lỗi giữa chừng trong quá trình dựng, hãy đọc getNumPages() để biết liệu có trang nào đã dựng được hay không, rồi gọi save() hoặc getPdfData() để xuất chúng ra. Engine cũng ghi lại các sự kiện hạ cấp không nghiêm trọng: getWarnings() trả về một list<NextPDF\Support\Warning>, hasWarnings() báo cáo liệu có cảnh báo nào được thu thập hay không, và hasDegradedParity() báo cáo liệu độ trung thực của kết quả có bị ảnh hưởng hay không. Các phương thức này cho phép một quy trình khôi phục phân biệt “thành công trọn vẹn” với “thành công nhưng độ trung thực bị giảm” mà không cần phân tích ngoại lệ.

Chính sách hạ cấp kiểm soát sự kiện nào bạn xử lý như ngoại lệ và sự kiện nào bạn xử lý như cảnh báo. NextPDF\Core\Config mặc định là DegradationPolicy::Balanced; chế độ này cảnh báo và tiếp tục với mức hạ cấp có giới hạn, nhưng ném ngoại lệ khi gặp tác động gây tắc nghẽn. DegradationPolicy::Permissive không bao giờ ném ngoại lệ và thu thập mọi thứ vào kênh cảnh báo. DegradationPolicy::Strict ném ngoại lệ khi gặp bất kỳ rủi ro tuân thủ, mất ngữ nghĩa, hoặc tác động gây tắc nghẽn nào. Hãy chọn chính sách trước, rồi viết cơ chế khôi phục cho các dạng lỗi mà chính sách đó có thể tạo ra.

Mã khôi phục dưới đây sử dụng các thành viên sau đã được kiểm chứng:

  • 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, và addFontDirectory(string $directory): static.
  • NextPDF\Core\Config::withDegradationPolicy(DegradationPolicy $policy): self và giá trị mặc định degradationPolicyDegradationPolicy::Balanced.
  • NextPDF\Contracts\DegradationPolicyStrict, Balanced, Permissive.
  • NextPDF\Exception\NextPdfException (lớp cơ sở trừu tượng), NextPDF\Exception\FontNotFoundException, NextPDF\Exception\HtmlParsingException, NextPDF\Exception\CssResolutionBudgetExceededException, NextPDF\Exception\WriterException, NextPDF\Exception\PageLayoutException.
  • NextPDF\Support\DegradedException (mang theo capabilitypolicy), NextPDF\Support\Capability (id, status, reason, isDegraded()), NextPDF\Support\Warning, NextPDF\Support\WarningSeverity.

Cơ chế khôi phục nhỏ nhất nhưng hữu ích sẽ bắt lỗi thiếu phông chữ, chuyển sang một phông chữ được bảo đảm khả dụng, rồi tiếp tục. Đoạn mã này lược bỏ phần xử lý rộng hơn trong mẫu dành cho môi trường thực tế. Để xem một bộ xử lý hoàn chỉnh có ghi nhật ký và ranh giới DegradedException, hãy đọc mẫu dành cho môi trường thực tế bên dưới.

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

Ví dụ đầy đủ kết hợp cả bốn chiến lược trong một đường kết xuất duy nhất: cơ chế dự phòng phông chữ, cơ chế chuyển bộ kết xuất từ đường trong tiến trình sang Chrome, một lần thử lại với HTML thay thế, và khôi phục tài liệu một phần dựa trên getNumPages(). Ví dụ này tôn trọng kênh đầu ra của bộ khung kiểm thử và không bao giờ bắt một Exception trần hoặc để trống khối 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 được để trống cho bộ khung kiểm thử. Thông tin chẩn đoán khôi phục được đưa vào STDERR, và tệp Portable Document Format (PDF) chỉ được ghi vào NEXTPDF_COOKBOOK_OUTPUT.

  • Hãy sắp xếp các khối catch từ cụ thể đến tổng quát. PHP khớp với khối catch tương thích đầu tiên. Đặt catch (NextPdfException $e) trước catch (WriterException $e) sẽ biến khối cụ thể thành mã chết, vì WriterException kế thừa NextPdfException.
  • DegradedException nằm ngoài hệ thống phân cấp. Nó kế thừa RuntimeException, chứ không phải NextPdfException. Một đường xử lý chỉ bắt NextPdfException sẽ để một lần từ chối theo chính sách strict lan truyền mà không bị bắt. Hãy bắt DegradedException (hoặc một RuntimeException rộng hơn) khi đang dùng một chính sách hạ cấp không phải mặc định.
  • Một cơ chế dự phòng phông chữ cũng có thể thất bại. Nếu ngay cả phông chữ dự phòng của bạn cũng chưa được đăng ký, lần gọi setFont() thứ hai sẽ lại ném ngoại lệ. Hãy dùng một bí danh Base14 chẳng hạn như helvetica, phông chữ mà engine xử lý được mà không cần tra cứu hệ thống tệp, hoặc đăng ký một phông chữ đi kèm qua addFontDirectory() khi khởi động để cơ chế dự phòng được bảo đảm.
  • getNumPages() tính cả trang hiện hoạt chưa được ghi ra. Nó trả về số trang đã ghi ra cộng thêm một khi đang có một trang mở. Một “lần lưu một phần” bao gồm cả trang đang được dựng khi lỗi xảy ra; đây thường là điều bạn mong muốn. Nếu bạn chỉ cần các trang đã hoàn thành đầy đủ, hãy phân nhánh dựa trên getPage() nữa.
  • Cơ chế dự phòng Chrome thay đổi độ trung thực, chứ không chỉ tính khả dụng. Đường xử lý trong tiến trình và cầu nối Chrome sử dụng các engine bố cục khác nhau, nên một tài liệu chuyển sang dùng Chrome có thể trông khác đi. Hãy xem cơ chế dự phòng là một biện pháp khôi phục, chứ không phải một phương án thay thế trong suốt, và ghi lại đường nào đã tạo ra kết quả.
  • Một lần thử lại phải sử dụng dữ liệu đầu vào chắc chắn hợp lệ. Lần thử lại với HTML đã đơn giản hóa chỉ hữu ích khi biến thể đã đơn giản hóa thực sự đơn giản hơn: ít bộ chọn lồng nhau hơn, không có chuỗi :has() nào làm cạn kiệt ngân sách phân giải. Thử lại với cùng dữ liệu đầu vào vốn đã thất bại sẽ lặp lại đúng ngoại lệ đó.
  • Hãy kiểm tra các cảnh báo sau một lần chạy không ném ngoại lệ. Một lần kết xuất trả về mà không ném ngoại lệ vẫn có thể đã bị hạ cấp. Hãy kiểm tra hasDegradedParity() và đọc getWarnings() trước khi bạn coi kết quả là trung thực đến từng pixel; với DegradationPolicy::Permissive, mọi lần hạ cấp đều là một cảnh báo, không bao giờ là một ngoại lệ.
  • Việc khôi phục chỉ làm tăng chi phí trên đường xử lý lỗi. NextPDF ném ngoại lệ ở các trạng thái bất thường, nên một lần kết xuất không lỗi không tốn gì cho khối try/catch bao quanh.
  • Một cơ chế dự phòng bộ kết xuất sẽ chạy lại lần kết xuất. Lần thử trong tiến trình bị loại bỏ và lần thử bằng Chrome bắt đầu lại từ đầu, nên trong trường hợp xấu nhất, một lần kết xuất dự phòng tốn chi phí bằng cả hai lần kết xuất cộng với vòng giao tiếp liên tiến trình với Chrome. Hãy tính đến nó khi bạn đặt thời gian chờ cho yêu cầu.
  • Một lần thử lại với HTML thay thế sẽ phân tích cú pháp một tài liệu thứ hai. Hãy giữ cho biến thể đã đơn giản hóa nhỏ gọn để lần thử lại rẻ hơn so với lần thử chính.
  • Một lần lưu một phần sẽ tuần tự hóa các trang đã được dựng. Chi phí của nó tỷ lệ với số trang đã dựng được, chứ không phải với phần việc đã thất bại.
  • Không hiển thị thông báo ngoại lệ nguyên gốc hoặc đường dẫn hệ thống tệp cho người dùng cuối. Thông báo của FontNotFoundException bao gồm các thư mục đã tìm kiếm và WriterException bao gồm đường dẫn đầu ra; cả hai đều làm lộ cấu trúc hệ thống tệp của máy chủ. Hãy ghi nhật ký ngữ cảnh có cấu trúc ở phía máy chủ và trả về một thông báo chung chung cho bên gọi.
  • Hãy coi HTML dùng để thử lại là dữ liệu đầu vào không đáng tin cậy trong mọi lần thử. Cơ chế dự phòng và lần thử lại với HTML đã đơn giản hóa đều đi qua cùng một ranh giới đầu vào; đường xử lý trong tiến trình và cầu nối Chrome mỗi đường áp dụng chính sách bảo mật HTML riêng, và một lần thử lại không nới lỏng việc kiểm tra đó. Không giả định rằng một biến thể “đã đơn giản hóa” thì an toàn hơn chỉ vì bạn là người tạo ra nó.
  • Một lần lưu một phần vẫn ghi ra một tệp. Hãy áp dụng cho kết quả một phần cùng các quy tắc kiểm tra đường dẫn, quyền, và vị trí lưu trữ mà bạn áp dụng cho kết quả hoàn chỉnh. Document::save() từ chối các stream wrapper và byte null và phân giải thư mục cha để chặn duyệt đường dẫn, nhưng đích đến mà bạn truyền vào là trách nhiệm của bạn.

Công thức này không đưa ra tuyên bố nào về tiêu chuẩn quy phạm. Nó kết hợp các API ngoại lệ công khai của NextPDF và các API kiểm tra tài liệu thành luồng điều khiển khôi phục; nó không khẳng định hành vi nào được định nghĩa bởi ISO 32000-2 hay bất kỳ tiêu chuẩn nào khác, nên không có khối citations:.

Trang này được kiểm chứng với hồ sơ khả tái lập semantic. Tài liệu đã khôi phục mang theo một /ID trong trailer và một ngày sửa đổi được tạo lại theo từng lần lưu, nên không thể đạt được sự đồng nhất ở mức byte. Phép so sánh cấu trúc bằng cây cú pháp trừu tượng (abstract syntax tree, AST), cộng với phép so sánh chỉ phần siêu dữ liệu, ổn định qua các lần chạy.