Salta ai contenuti
getnextpdf.com

Gli errori come funzionalità

Spec: ISO 9241-110, §5.6.4

NextPDF tratta la propria gerarchia di eccezioni come una superficie API, progettata con la stessa cura riservata ai metodi che la sollevano. Un guasto è specifico, tipizzato, intercettabile con la granularità necessaria e trasporta un contesto strutturato per i log.

Questa pagina mostra tale superficie nel codice sorgente del motore stesso: il tipo base, le sottoclassi tipizzate, i costruttori con nome che legano una causa radice al messaggio e il contesto strutturato che ogni eccezione NextPDF espone.

Un messaggio di errore è il motore che parla nel momento peggiore possibile: in produzione, alle 2 di notte, con un documento che sarebbe già dovuto uscire. Ciò che quel messaggio dice in quel momento decide se il passo successivo sarà una correzione o una lunga indagine.

Un generico RuntimeException: something went wrong non porta da nessuna parte. Dice che il motore ha fallito, ma non cosa è fallito, non dove e di certo non cosa fare. Le linee guida ergonomiche sono esplicite in proposito. Un errore dovrebbe spiegarsi abbastanza bene da rendere la sua correzione l’ovvio passo successivo, non un progetto di ricerca (Spec: ISO 9241-110, §5.6.4.3). Un’eccezione che nomina la causa e il rimedio non è un vezzo. È la differenza tra una correzione di cinque minuti e una di cinque ore.

  • Ogni guasto di NextPDF estende un’unica base astratta, NextPdfException, così da poter intercettare tutti gli errori della libreria con un solo tipo.
  • Al di sotto stanno sottoclassi specifiche e tipizzate — un font che non si trova, una configurazione non valida, un’operazione di firma fallita — così da poter intercettare esattamente il guasto che si è in grado di gestire.
  • Ogni eccezione NextPDF implementa ContextAwareExceptionInterface ed espone getContext(): una mappa strutturata e sicura per i log, così da non dover mai analizzare una stringa di messaggio per recuperare i dati diagnostici.
  • I messaggi sono azionabili: i costruttori con nome legano al messaggio la reale causa radice (e spesso la correzione), invece di un modello generico.
  • Ogni classe di eccezione documenta chi può intervenire — sviluppatore, infrastruttura o chiamante della libreria — così il triage inizia prima di leggere lo stack trace.

La gerarchia è poco profonda e deliberata. C’è una sola base, uno strato di tipi specifici del dominio e un contratto che ognuno di essi rispetta.

Una sola base, catch-all per progettazione. NextPdfException è astratta, estende RuntimeException e implementa ContextAwareExceptionInterface:

abstract class NextPdfException extends RuntimeException implements ContextAwareExceptionInterface
{
/** @return array<string, mixed> */
public function getContext(): array
{
return [];
}
}

Renderla astratta è una decisione. Non si intercetta mai la vaga base per caso, perché non viene mai sollevata direttamente. La si intercetta deliberatamente, come rete di sicurezza, e si intercetta una sottoclasse specifica quando si può fare qualcosa di specifico.

Sottoclassi specifiche e tipizzate. Un font mancante non è un errore generico; è FontNotFoundException, e trasporta i dati necessari per intervenire:

final class FontNotFoundException extends NextPdfException
{
public function __construct(
private readonly string $fontName,
private readonly array $searchPaths,
private readonly bool $fallbackAttempted,
?Throwable $previous = null,
) {
parent::__construct(
\sprintf('Font "%s" not found. Searched: [%s].', $fontName, \implode(', ', $searchPaths)),
0,
$previous,
);
}
// getFontName(), getSearchPaths(), wasFallbackAttempted(), getContext()
}

Il messaggio nomina il font e i percorsi esatti in cui è stato cercato. Non si tira a indovinare quale directory mancasse; è l’eccezione a dirlo.

Contesto strutturato, non estrazione da stringhe. Ogni eccezione restituisce una mappa in snake_case, composta solo da primitivi, sicura da serializzare direttamente in un log o in un payload APM:

public function getContext(): array
{
return [
'config_key' => $this->configKey,
'given_value' => $this->givenValue,
'expected_type' => $this->expectedType,
];
}

Il contratto è esplicito sul perché. Un middleware di logging può chiamare $logger->error($e->getMessage(), $e->getContext()) per qualsiasi eccezione NextPDF senza mai analizzare il messaggio. Il messaggio è per gli esseri umani. Il contesto è per le macchine. Nessuno dei due deve fare le veci dell’altro.

Messaggi azionabili tramite costruttori con nome. È qui che gli errori smettono di essere incidentali e diventano progettati. SignatureException non si limita a dire «la firma è fallita al livello B-LT». Offre costruttori con nome che legano al messaggio la reale causa radice e, spesso, il rimedio esatto:

public static function tsaUrlEmpty(string $signatureLevel): self
{
return new self('', $signatureLevel, null,
'TSA endpoint URL is empty: pass a non-empty `tsaUrl` to the TsaClient '
. 'constructor (e.g. "https://timestamp.example.com/tsa") or remove the '
. 'TSA client wiring if no timestamping is required at this signature level');
}

Il messaggio dichiara che cosa non va e cosa farci. Esistono costruttori affini per un pacchetto di funzionalità mancante, un client HTTP assente, un algoritmo digest-only scelto per errore, un tipo di chiave che non corrisponde all’algoritmo e altro ancora. Ognuno trasforma una classe di guasti in una frase su cui uno sviluppatore può agire senza leggere il codice sorgente del motore.

Guasti volutamente rumorosi. Alcune eccezioni esistono proprio perché una lacuna silenziosa diventi rumorosa. NotImplementedException porta un’etichetta feature ricercabile automaticamente via grep e un riferimento followUp:

final class NotImplementedException extends NextPdfException
{
public function __construct(
public readonly string $feature,
public readonly string $followUp,
?Throwable $previous = null,
) {
parent::__construct(
\sprintf('%s is not implemented in this release. %s', $feature, $followUp),
0, $previous,
);
}
}

Un percorso raggiunto ma non cablato solleva questa eccezione invece di restituire un no-op plausibile. La stessa idea guida StrictModeViolation, le cui sottoclassi portano una breve etichetta ricercabile via grep per il costrutto deviante, oltre a un contesto opzionale di posizione e citazione. Una deviazione dalla specifica diventa un arresto tipizzato e contestuale, non un render silenziosamente errato.

Metadati di triage nella classe stessa. Ogni classe di eccezione nomina chi può intervenire nel proprio docblock. Ad esempio, FontNotFoundException è «Developer (verify font path) or Infrastructure (fix file permissions)». InvalidConfigException è «Developer (fix configuration before calling NextPDF)». NotImplementedException è «Library callers — either remove the call or pin to a future release». Il triage inizia prima dello stack trace, perché la domanda «è un problema mio o delle operations?» ha già una risposta scritta.

La tabella riassume la progettazione e ciò che ogni proprietà offre.

Proprietà di progettazioneNel codice sorgenteCosa offre
Un’unica base astrattaNextPdfException (astratta, implementa l’interfaccia di contesto)Intercettare ogni errore della libreria con un solo tipo, mai la vaga base per caso
Sottoclassi tipizzate specificheFontNotFoundException, InvalidConfigException, SignatureException, …Intercettare esattamente il guasto che si è in grado di gestire
Contesto strutturatogetContext() — solo primitivi in snake_caseRegistrare o inviare all’APM senza analizzare una stringa di messaggio
Messaggi azionabiliI costruttori con nome legano causa radice + rimedioUna frase su cui agire, non un modello
Volutamente rumorosiNotImplementedException, StrictModeViolationUna lacuna silenziosa diventa un arresto tipizzato e ricercabile via grep
Metadati di triage«Actionable by:» in ogni docblock di classeSapere di chi è il problema prima di leggere il trace

Intercettare in modo ampio come rete di sicurezza, intercettare in modo specifico dove si può agire e fornire il contesto strutturato direttamente al logger — senza analisi del messaggio.

<?php
declare(strict_types=1);
use NextPDF\Core\Document;
use NextPDF\Exception\FontNotFoundException;
use NextPDF\Exception\NextPdfException;
use Psr\Log\LoggerInterface;
function renderInvoice(LoggerInterface $logger): ?string
{
try {
$document = Document::createStandalone();
$document->setTitle('Invoice 2026-0042');
$document->addPage();
$document->setFont('BrandSans', '', 12);
$document->cell(0, 10, 'Thank you for your business.', newLine: true);
return $document->getPdfData();
} catch (FontNotFoundException $e) {
// Specific: we can recover — fall back to a built-in font.
// getContext() is log-safe structured data, not a parsed string.
$logger->warning($e->getMessage(), $e->getContext());
return null; // caller re-renders with 'helvetica'
} catch (NextPdfException $e) {
// Backstop: any other NextPDF failure, still with structured context.
$logger->error($e->getMessage(), $e->getContext());
return null;
}
}

Il catch specifico effettua il recupero perché il tipo di eccezione gli ha indicato che il recupero era possibile. La rete di sicurezza registra il contesto strutturato per tutto il resto. In nessun momento l’applicazione legge il messaggio per scoprire che cosa è successo.

L’errore di lettura consueto è che un albero di eccezioni profondo sia sovraingegnerizzazione e che un unico tipo di errore sarebbe più semplice. Sarebbe più semplice per il motore e peggiore per chi lo usa. Un solo tipo significa che ogni guasto è un generico stack trace e che la logica di recupero è una corrispondenza di stringhe. Quella corrispondenza è fragile; la prossima riformulazione del messaggio la rompe. Una gerarchia piccola e specifica sposta quella conoscenza nel sistema dei tipi, dove il compilatore e i blocchi catch possono usarla.

Un secondo equivoco è che il messaggio e il contesto siano ridondanti. Non lo sono. Il messaggio è prosa per un essere umano che legge una riga di log. Il contesto è una mappa tipizzata per l’instradamento nel codice, l’alerting o le dashboard. Confonderli è esattamente la trappola dell’analisi di stringhe che il contratto di getContext() esiste proprio per eliminare.

La gerarchia è intenzionalmente poco profonda. NextPDF non crea una classe di eccezione distinta per ogni guasto immaginabile. Ne crea una quando intercettare quel guasto in modo specifico è qualcosa che un chiamante farebbe ragionevolmente. Suddividere troppo scambierebbe il problema dell’analisi di stringhe con un dilagante problema di elenchi di catch.

getContext() è strutturato per i log e l’APM, quindi per contratto restituisce solo primitivi ed elenchi di primitivi, senza oggetti annidati. È contesto diagnostico, non un’istantanea serializzata degli interni del motore. Non è nemmeno un formato di trasmissione stabile su cui costruire schemi esterni.

Questa pagina descrive la superficie di progettazione delle eccezioni. L’insieme esatto delle eccezioni e dei loro campi evolve con il motore. Le classi e le forme qui citate sono aggiornate alla data di questa revisione e sono illustrative del contratto, non un catalogo congelato. Il contratto — una base, sottoclassi tipizzate, contesto strutturato, messaggi azionabili — è la parte stabile.

  • Code-backed (livello di evidenza) — una pagina le cui affermazioni sono verificate rispetto al codice sorgente del motore stesso, citato anziché parafrasato.
  • Eccezione context-aware — un’eccezione NextPDF che implementa ContextAwareExceptionInterface ed espone getContext(). Quel metodo restituisce una mappa in snake_case di campi diagnostici primitivi, sicura da serializzare in un log o in un payload APM senza analizzare la stringa di messaggio.
  • Costruttore con nome — un metodo factory statico (ad esempio SignatureException::tsaUrlEmpty()) che costruisce un’eccezione con un messaggio legato a una specifica causa radice e, spesso, al suo rimedio.
  • PAdES — PDF Advanced Electronic Signatures, la famiglia di profili ETSI per la firma di PDF. Reso per esteso al primo utilizzo; trattato in dettaglio nelle pagine sulla firma.
  • TSA — Time-Stamping Authority, il servizio attendibile che emette i timestamp RFC 3161 utilizzati dai profili PAdES superiori.