Pular para o conteúdo

Implemente estratégias personalizadas de recuperação de erros e repetição

Um serviço de documentos em produção faz mais do que capturar e registrar uma exceção. Ele decide o que fazer em seguida: continuar com uma saída degradada, alternar para um segundo caminho de renderização, repetir com uma entrada aceita pelo engine ou entregar as páginas construídas antes da falha. Esta receita mostra quatro estratégias de recuperação construídas sobre a hierarquia de exceções do NextPDF e os métodos de inspeção do estado do documento:

  • Degradação controlada em uma falha de fonte — capture NextPDF\Exception\FontNotFoundException, faça fallback para uma face garantida e continue construindo o documento.
  • Um renderizador alternativo — quando o caminho in-process Document::writeHtml() rejeitar a entrada, repita por meio de Document::writeHtmlChrome(), a ponte com o Chrome do nextpdf/artisan.
  • Repetir com HTML alternativo — quando ocorrer NextPDF\Exception\HtmlParsingException ou NextPDF\Exception\CssResolutionBudgetExceededException, repita com uma variante simplificada e comprovadamente válida de HTML.
  • Recuperação de documento parcial — leia Document::getNumPages() após uma falha e salve o que já foi construído, em vez de descartá-lo.

Você já sabe como capturar no nível certo. A página complementar Trate erros com a hierarquia de exceções do NextPDF aborda a hierarquia em si. Esta página mostra o que você faz depois da captura.

Esta receita tem como alvo a edição core de software de código aberto (OSS). Toda interface de programação de aplicações (API) citada aqui está em nextpdf/core. A única dependência opcional é nextpdf/artisan para a alternativa com Chrome.

Terminal window
composer require nextpdf/core:^3

A estratégia de renderizador alternativo usa adicionalmente a ponte com o Chrome:

Terminal window
composer require nextpdf/artisan

Quando nextpdf/artisan está ausente, Document::writeHtmlChrome() lança NextPDF\Exception\PageLayoutException em vez de renderizar. A estratégia alternativa a seguir trata a ausência da ponte como mais um caso recuperável.

A recuperação depende de dois fatos sobre o NextPDF, ambos verificados em relação ao código-fonte.

A hierarquia de exceções informa o que é recuperável. Toda exceção de domínio estende a base abstrata NextPDF\Exception\NextPdfException, que estende RuntimeException e implementa NextPDF\Contracts\ContextAwareExceptionInterface. Capture um subtipo específico para escolher um caminho de recuperação adequado àquela falha:

  • FontNotFoundException carrega getFontName(), getSearchPaths() e wasFallbackAttempted() — suficiente para repetir com uma face diferente.
  • HtmlParsingException carrega getRule(), getPosition() e getHtmlSnippet() — suficiente para decidir se vale a pena tentar novamente com uma versão simplificada.
  • CssResolutionBudgetExceededException carrega getVisits() e getBudget() — um sinal de que uma folha de estilos reduzida pode liberar um seletor patológico.
  • Um limite importante: NextPDF\Support\DegradedException estende RuntimeException diretamente, não NextPdfException. Portanto, catch (NextPdfException $e) não captura uma rejeição da política de degradação. Quando a NextPDF\Contracts\DegradationPolicy ativa é Strict ou Balanced, capture DegradedException explicitamente para recuperar-se dela.

O documento pode ser inspecionado enquanto você o constrói. Um Document expõe seu estado de construção por meio de acessores somente leitura. getNumPages() retorna a contagem total de páginas, incluindo a página ativa ainda não descarregada, e getPage() retorna o índice de base zero da página atual. Após uma falha no meio da construção, leia getNumPages() para descobrir se existem páginas completas e, em seguida, chame save() ou getPdfData() para emiti-las. O engine também registra eventos de degradação não fatais: getWarnings() retorna uma list<NextPDF\Support\Warning>, hasWarnings() informa se alguma foi coletada e hasDegradedParity() informa se a fidelidade da saída foi afetada. Esses métodos permitem que uma rotina de recuperação diferencie “concluído sem problemas” de “concluído com fidelidade reduzida” sem precisar analisar uma exceção.

A política de degradação controla quais eventos você trata como exceções e quais trata como avisos. NextPDF\Core\Config usa por padrão DegradationPolicy::Balanced, que avisa e continua em degradações limitadas, mas lança quando há impacto bloqueante. DegradationPolicy::Permissive nunca lança e coleta tudo no canal de avisos. DegradationPolicy::Strict lança diante de qualquer risco de conformidade, perda semântica ou impacto bloqueante. Escolha primeiro a política e, em seguida, escreva a recuperação para os formatos de falha que ela produz.

O código de recuperação abaixo usa estes membros verificados:

  • 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 o padrão de degradationPolicy igual a DegradationPolicy::Balanced.
  • NextPDF\Contracts\DegradationPolicyStrict, Balanced, Permissive.
  • NextPDF\Exception\NextPdfException (base abstrata), NextPDF\Exception\FontNotFoundException, NextPDF\Exception\HtmlParsingException, NextPDF\Exception\CssResolutionBudgetExceededException, NextPDF\Exception\WriterException, NextPDF\Exception\PageLayoutException.
  • NextPDF\Support\DegradedException (carregando capability e policy), NextPDF\Support\Capability (id, status, reason, isDegraded()), NextPDF\Support\Warning, NextPDF\Support\WarningSeverity.

A menor recuperação útil captura uma falha de fonte ausente, faz fallback para uma face garantida e continua. Este trecho deixa de fora o tratamento mais amplo presente no exemplo de produção. Para um manipulador completo com logs e o limite de DegradedException, leia o exemplo de produção abaixo.

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

O exemplo completo conecta as quatro estratégias em um único pipeline de renderização: uma alternativa de fonte, uma alternativa de renderizador do caminho in-process para o Chrome, uma repetição com HTML alternativo e a recuperação de documento parcial conduzida por getNumPages(). Ele respeita o canal de saída do harness e nunca captura uma Exception genérica nem deixa um bloco catch vazio.

<?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 permanece livre para o harness. Os diagnósticos de recuperação vão para STDERR, e o arquivo Portable Document Format (PDF) só é gravado em NEXTPDF_COOKBOOK_OUTPUT.

  • Ordene os blocos catch do específico para o geral. O PHP usa o primeiro catch compatível. Colocar catch (NextPdfException $e) antes de catch (WriterException $e) transforma o bloco específico em código morto, porque WriterException estende NextPdfException.
  • DegradedException fica fora da hierarquia. Ela estende RuntimeException, não NextPdfException. Um pipeline que captura apenas NextPdfException deixa uma rejeição de política estrita se propagar sem captura. Capture DegradedException (ou uma RuntimeException mais ampla) quando uma política de degradação diferente da padrão estiver ativa.
  • Uma fonte alternativa também pode falhar. Se a face alternativa não estiver registrada, o segundo setFont() lança novamente. Use um alias Base14 como helvetica, que o engine resolve sem uma consulta ao sistema de arquivos, ou registre uma face empacotada por meio de addFontDirectory() na inicialização para que a alternativa seja garantida.
  • getNumPages() conta a página ativa ainda não descarregada. Ela retorna a contagem de páginas descarregadas mais um quando uma página está aberta no momento. Um “salvamento parcial” inclui a página que estava sendo construída quando a falha ocorreu, o que normalmente é o que você quer. Se você precisar apenas de páginas totalmente concluídas, ramifique também com base em getPage().
  • A alternativa do Chrome altera a fidelidade, não apenas a disponibilidade. O pipeline in-process e a ponte com o Chrome usam engines de layout diferentes, então um documento que recorre ao Chrome pode ter aparência diferente. Trate a alternativa como uma recuperação, não como um substituto transparente, e registre qual caminho produziu a saída.
  • Uma repetição deve usar uma entrada comprovadamente válida. A repetição com HTML simplificado só ajuda quando a variante simplificada é genuinamente mais simples: menos seletores aninhados, sem cadeias de :has() que esgotem o orçamento de resolução. Repetir com a mesma entrada que já falhou leva à mesma exceção em loop.
  • Inspecione os avisos após uma execução sem problemas. Uma renderização que retorna sem lançar ainda pode ter sido degradada. Verifique hasDegradedParity() e leia getWarnings() antes de tratar a saída como fiel pixel a pixel; sob DegradationPolicy::Permissive toda degradação é um aviso, nunca uma exceção.
  • A recuperação acrescenta custo apenas no caminho de falha. O NextPDF lança em estados excepcionais, então uma renderização sem problemas não paga nada pelo try/catch ao redor.
  • Uma alternativa de renderizador executa a renderização novamente. A tentativa in-process é descartada e a tentativa do Chrome começa do zero; portanto, uma renderização alternativa custa, no pior caso, ambos os tempos de renderização mais a ida e volta entre processos até o Chrome. Considere isso ao definir os tempos limite de requisição.
  • Uma repetição com HTML alternativo analisa um segundo documento. Mantenha a variante simplificada pequena para que a repetição seja barata em relação à tentativa primária.
  • Um salvamento parcial serializa as páginas já construídas. Seu custo escala com a contagem de páginas restantes, não com o trabalho que falhou.
  • Não exiba mensagens de exceção brutas nem caminhos do sistema de arquivos aos usuários finais. A mensagem de uma FontNotFoundException inclui os diretórios pesquisados, e uma WriterException inclui o caminho de saída; ambas expõem a estrutura do servidor. Registre o contexto estruturado no servidor e retorne uma mensagem genérica ao chamador.
  • Trate o HTML repetido como entrada não confiável em toda tentativa. A alternativa e a repetição com HTML simplificado fluem pelo mesmo limite de entrada; o pipeline in-process e a ponte com o Chrome aplicam cada um sua própria política de segurança de HTML, e uma repetição não relaxa essa validação. Não presuma que uma variante “simplificada” seja mais segura só porque você a criou.
  • Um salvamento parcial ainda grava um arquivo. Aplique a uma saída parcial as mesmas regras de validação de caminho, permissões e local de armazenamento que você aplica a uma saída completa. Document::save() rejeita stream wrappers e bytes nulos e resolve o diretório pai para bloquear a travessia de caminho (path traversal), mas o destino que você passa é responsabilidade sua.

Esta receita não faz nenhuma afirmação normativa sobre padrões. Ela compõe as APIs públicas de exceção e de inspeção de documentos do NextPDF em um fluxo de controle de recuperação; não afirma um comportamento definido pela ISO 32000-2 nem por qualquer outro padrão, portanto não traz um bloco citations:.

Esta página é verificada com o perfil de reprodutibilidade semantic. O documento recuperado carrega um /ID no trailer e uma data de modificação que são regerados a cada salvamento; portanto, a identidade byte a byte não é alcançável. A comparação da árvore de sintaxe abstrata (AST) estrutural mais somente metadados é estável entre execuções.