Реализация собственных стратегий восстановления после ошибок и повторных попыток
Промышленный сервис генерации документов не ограничивается перехватом и журналированием исключений. Он решает, что делать дальше: продолжить с упрощённым результатом, переключиться на другой путь отрисовки, повторить попытку с входными данными, которые принимает движок, или выдать страницы, построенные до сбоя. Этот рецепт показывает четыре стратегии восстановления, основанные на иерархии исключений NextPDF и методах проверки состояния документа:
- Плавная деградация при сбое со шрифтом — перехватите
NextPDF\Exception\FontNotFoundException, переключитесь на гарантированный шрифт и продолжите построение документа. - Резервный рендерер — когда встроенный путь
Document::writeHtml()отклоняет входные данные, повторите попытку черезDocument::writeHtmlChrome()— мост к Chrome изnextpdf/artisan. - Повторная попытка с альтернативным HTML — когда возникает
NextPDF\Exception\HtmlParsingExceptionилиNextPDF\Exception\CssResolutionBudgetExceededException, повторите попытку с упрощённым, заведомо рабочим вариантом HTML. - Восстановление частичного документа — считайте
Document::getNumPages()после сбоя и сохраните уже построенное, вместо того чтобы отбросить результат.
Вы уже знаете, как перехватывать исключения на нужном уровне. Сопутствующая страница Обработка ошибок с помощью иерархии исключений NextPDF описывает саму иерархию. Эта страница показывает, что делать после перехвата.
Этот рецепт рассчитан на редакцию core с открытым исходным кодом (OSS). Каждый упомянутый здесь интерфейс программирования приложений (API) находится в nextpdf/core. Единственная опциональная зависимость — nextpdf/artisan для резервного перехода на Chrome.
Установка
Заголовок раздела «Установка»composer require nextpdf/core:^3Стратегия резервного рендерера дополнительно использует мост к Chrome:
composer require nextpdf/artisanЕсли nextpdf/artisan отсутствует, Document::writeHtmlChrome() вместо отрисовки выбрасывает NextPDF\Exception\PageLayoutException. Описанная ниже резервная стратегия считает отсутствующий мост ещё одним восстановимым случаем.
Концептуальный обзор
Заголовок раздела «Концептуальный обзор»Восстановление опирается на два факта о NextPDF; оба сверены с исходным кодом.
Иерархия исключений показывает, что можно восстановить. Каждое доменное исключение наследуется от абстрактной базы NextPDF\Exception\NextPdfException, которая наследуется от RuntimeException и реализует NextPDF\Contracts\ContextAwareExceptionInterface. Перехватывайте конкретный подтип, чтобы выбрать путь восстановления для данного сбоя:
FontNotFoundExceptionсодержитgetFontName(),getSearchPaths()иwasFallbackAttempted()— этого достаточно, чтобы повторить попытку с другим шрифтом.HtmlParsingExceptionсодержитgetRule(),getPosition()иgetHtmlSnippet()— этого достаточно, чтобы решить, стоит ли пробовать упрощённую повторную попытку.CssResolutionBudgetExceededExceptionсодержитgetVisits()иgetBudget()— это признак того, что упрощённая таблица стилей может устранить патологический селектор.- Важная граница:
NextPDF\Support\DegradedExceptionнаследуется отRuntimeExceptionнапрямую, а не отNextPdfException. Поэтомуcatch (NextPdfException $e)не перехватывает отказ политики деградации. Когда активная политикаNextPDF\Contracts\DegradationPolicy—StrictилиBalanced, перехватывайтеDegradedExceptionявно, чтобы восстановиться после него.
Документ можно проверять во время построения. Document раскрывает своё состояние построения через методы доступа только для чтения. getNumPages() возвращает общее число страниц, включая активную несброшенную страницу, а getPage() возвращает отсчитываемый от нуля индекс текущей страницы. После сбоя в середине построения считайте getNumPages(), чтобы понять, есть ли завершённые страницы, а затем вызовите save() или getPdfData(), чтобы вывести их. Движок также фиксирует нефатальные события деградации: getWarnings() возвращает list<NextPDF\Support\Warning>, hasWarnings() сообщает, были ли они собраны, а hasDegradedParity() сообщает, была ли затронута точность вывода. Эти методы позволяют процедуре восстановления отличить “успешно без замечаний” от “успешно с пониженной точностью” без разбора исключения.
Политика деградации определяет, какие события вы обрабатываете как исключения, а какие — как предупреждения. По умолчанию NextPDF\Core\Config использует DegradationPolicy::Balanced: при ограниченной деградации она предупреждает и продолжает работу, но выбрасывает исключение при блокирующем воздействии. DegradationPolicy::Permissive никогда не выбрасывает исключение и собирает всё в канале предупреждений. DegradationPolicy::Strict выбрасывает исключение при любом риске соответствия, потере семантики или блокирующем воздействии. Сначала выберите политику, а затем напишите восстановление для тех видов сбоев, которые она порождает.
Состав API
Заголовок раздела «Состав API»Приведённый ниже код восстановления использует следующие проверенные члены:
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и значение по умолчаниюdegradationPolicy, равноеDegradationPolicy::Balanced.NextPDF\Contracts\DegradationPolicy—Strict,Balanced,Permissive.NextPDF\Exception\NextPdfException(абстрактная база),NextPDF\Exception\FontNotFoundException,NextPDF\Exception\HtmlParsingException,NextPDF\Exception\CssResolutionBudgetExceededException,NextPDF\Exception\WriterException,NextPDF\Exception\PageLayoutException.NextPDF\Support\DegradedException(несущийcapabilityиpolicy),NextPDF\Support\Capability(id,status,reason,isDegraded()),NextPDF\Support\Warning,NextPDF\Support\WarningSeverity.
Пример кода — быстрый старт
Заголовок раздела «Пример кода — быстрый старт»Самое маленькое полезное восстановление перехватывает сбой из-за отсутствующего шрифта, переходит на гарантированный шрифт и продолжает работу. Этот фрагмент опускает более широкую обработку из промышленного примера. Полный обработчик с журналированием и границей DegradedException приведён в промышленном примере ниже.
<?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');Пример кода — промышленный
Заголовок раздела «Пример кода — промышленный»Полный пример объединяет все четыре стратегии в одном конвейере отрисовки: переход на резервный шрифт, переключение рендерера со встроенного пути на Chrome, повторную попытку с альтернативным HTML и восстановление частичного документа на основе getNumPages(). Он сохраняет канал вывода для обвязки, никогда не перехватывает голый Exception и не оставляет блок 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 остаётся свободным для обвязки. Диагностика восстановления отправляется в STDERR, а файл Portable Document Format (PDF) записывается только в NEXTPDF_COOKBOOK_OUTPUT.
Граничные случаи и подводные камни
Заголовок раздела «Граничные случаи и подводные камни»- Упорядочивайте блоки catch от частного к общему. PHP сопоставляет первый совместимый
catch. Размещениеcatch (NextPdfException $e)передcatch (WriterException $e)превращает частный блок в недостижимый код, потому чтоWriterExceptionнаследуется отNextPdfException. DegradedExceptionнаходится вне иерархии. Он наследуется отRuntimeException, а не отNextPdfException. Конвейер, перехватывающий толькоNextPdfException, пропускает отказ строгой политики наружу. ПерехватывайтеDegradedException(или более общийRuntimeException), когда активна недефолтная политика деградации.- Переход на резервный шрифт тоже может завершиться сбоем. Если резервный шрифт сам не зарегистрирован, второй вызов
setFont()снова выбрасывает исключение. Используйте псевдоним Base14, напримерhelvetica, который движок разрешает без обращения к файловой системе, либо зарегистрируйте поставляемый в комплекте шрифт черезaddFontDirectory()при запуске, чтобы резервный вариант был гарантирован. getNumPages()учитывает активную несброшенную страницу. Он возвращает число сброшенных страниц плюс одну, когда в данный момент открыта страница. “Частичное сохранение” включает страницу, которая строилась в момент сбоя, что обычно и требуется. Если вам нужны только полностью завершённые страницы, дополнительно ветвитесь поgetPage().- Переход на Chrome меняет точность, а не только доступность. Встроенный конвейер и мост к Chrome используют разные движки макета, поэтому документ, перешедший на Chrome, может выглядеть иначе. Рассматривайте резервный переход как восстановление, а не как прозрачную замену, и фиксируйте, какой путь сформировал вывод.
- Повторная попытка должна использовать заведомо рабочие входные данные. Повторная попытка с упрощённым HTML помогает только тогда, когда упрощённый вариант действительно проще: меньше вложенных селекторов, нет цепочек
:has(), исчерпывающих бюджет разрешения. Повторная попытка с теми же входными данными, которые уже привели к сбою, зациклится на том же исключении. - Проверяйте предупреждения после успешного прогона. Отрисовка, завершившаяся без исключения, всё же могла пройти с деградацией. Проверьте
hasDegradedParity()и считайтеgetWarnings(), прежде чем считать вывод попиксельно точным; приDegradationPolicy::Permissiveкаждая деградация — это предупреждение, а не исключение.
Производительность
Заголовок раздела «Производительность»- Восстановление добавляет затраты только на пути сбоя. NextPDF выбрасывает исключение в исключительных состояниях, поэтому обычная отрисовка ничего не платит за окружающий
try/catch. - Переход на резервный рендерер повторно запускает отрисовку. Встроенная попытка отбрасывается, а попытка через Chrome начинается заново, поэтому резервная отрисовка в худшем случае стоит времени обеих отрисовок плюс межпроцессный обмен с Chrome. Учитывайте это при настройке таймаутов запросов.
- Повторная попытка с альтернативным HTML разбирает второй документ. Делайте упрощённый вариант небольшим, чтобы повторная попытка была дешёвой по сравнению с основной.
- Частичное сохранение сериализует уже построенные страницы. Его стоимость растёт с числом сохранившихся страниц, а не с объёмом работы, завершившейся сбоем.
Замечания по безопасности
Заголовок раздела «Замечания по безопасности»- Не показывайте конечным пользователям необработанные сообщения исключений или пути файловой системы. Сообщение
FontNotFoundExceptionвключает каталоги, в которых выполнялся поиск, аWriterExceptionвключает путь вывода; оба раскрывают структуру сервера. Журналируйте структурированный контекст на стороне сервера и возвращайте вызывающей стороне обобщённое сообщение. - Рассматривайте HTML при повторной попытке как недоверенные входные данные при каждой попытке. И резервный переход, и повторная попытка с упрощённым HTML проходят через одну и ту же границу ввода; встроенный конвейер и мост к Chrome применяют собственные политики безопасности HTML, и повторная попытка не ослабляет эту проверку. Не считайте “упрощённый” вариант более безопасным только потому, что вы его создали.
- Частичное сохранение всё равно записывает файл. Применяйте к частичному выводу те же правила проверки пути, разрешений и места хранения, что и к полному.
Document::save()отклоняет обёртки потоков и нулевые байты и разрешает родительский каталог, чтобы заблокировать обход пути, но место назначения, которое вы передаёте, остаётся вашей ответственностью.
Соответствие
Заголовок раздела «Соответствие»Этот рецепт не заявляет соответствие нормативным стандартам. Он объединяет публичные API исключений и проверки документа NextPDF в поток управления восстановлением; он не утверждает поведение, определённое ISO 32000-2 или каким-либо другим стандартом, поэтому не содержит блока citations:.
Эта страница проверена с профилем воспроизводимости semantic. Восстановленный документ содержит в трейлере /ID и дату изменения, которые регенерируются при каждом сохранении, поэтому побайтовая идентичность недостижима. Сравнение структурного абстрактного синтаксического дерева (AST) вместе только с метаданными стабильно между запусками.
См. также
Заголовок раздела «См. также»- Обработка ошибок с помощью иерархии исключений NextPDF — гранулярность перехвата и структурированный контекст, основа, на которой строится эта страница.
- Модуль Exception — полный справочник по исключениям.
- Модуль Support —
DegradedException,Capability,Warningи типы деградации. - Модуль Config — настройка политики деградации.
- Безопасная отрисовка PDF в долгоживущем воркере — восстановление в воркере, который повторно использует общие реестры.