Salta ai contenuti

Un'API che si rifiuta di tirare a indovinare

Spec: ISO/IEC 25010 Spec: ISO 32000-2 Evidence: Code-backed

NextPDF richiede di dichiarare l’intento. Quando l’intento cambia i byte — un livello di firma, una destinazione di output, un obiettivo di conformità — deve essere un argomento esplicito e obbligatorio, non qualcosa che il motore deduce dal contesto.

Questa pagina mostra questa posizione nel codice sorgente del motore stesso: le firme dei metodi, gli argomenti con nome e i punti in cui un input ambiguo viene rifiutato prima che venga prodotto un solo byte.

Un’ipotesi è una decisione presa al posto del chiamante senza renderlo esplicito. In un campo di testo può essere solo un lieve fastidio. In un PDF è un difetto latente, perché ciò che viene distribuito è spesso un artefatto legale o di archiviazione la cui correttezza sarà verificata in seguito, da qualcun altro, con un validatore.

Si consideri una firma. Il suo digest viene calcolato su un intervallo di byte dichiarato che esclude deliberatamente lo stesso valore della firma ( Spec: ISO 32000-2, §12.8 ). Un’API che «aiuta» silenziosamente — riscrivendo la struttura, deducendo un livello, riempiendo un segnaposto — non ha aiutato. Ha modificato i byte che una firma avrebbe dovuto proteggere. L’ipotesi che sembra amichevole al punto di chiamata diventa, settimane dopo, l’incidente in produzione. È la stessa riga di codice.

  • Se una scelta cambia l’output e non ha un valore predefinito sicuro, NextPDF ne fa un argomento obbligatorio, non uno dedotto.
  • Gli argomenti facoltativi dalla lettura ambigua sono con nome, così il punto di chiamata dichiara l’intento (newLine: true, non un nudo true).
  • Gli input potenzialmente non sicuri sono convalidati prima del rendering e rifiutati con un’eccezione tipizzata che ne nomina la causa.
  • Un’istanza di documento è a uso singolo: viene costruita, emessa e scartata. Non esiste alcun reset(), quindi non resta alcuna ipotesi del tipo «questo oggetto viene riutilizzato?».
  • Il motore non emette mai un artefatto dall’aspetto plausibile al posto di quello richiesto: si rifiuta.

Il meccanismo è discreto, ed è proprio questo il punto. È il sistema dei tipi, gli argomenti con nome, gli enum al posto delle stringhe magiche e un piccolo numero di clausole di guardia deliberate poste prima dell’output.

La tabella mette a confronto alcuni input ambigui. Per ciascuno mostra ciò che una libreria che «aiuta» dedurrebbe e ciò che NextPDF fa invece. Ogni colonna NextPDF è un comportamento citato dal codice sorgente mostrato più avanti in questa pagina.

Input ambiguoCosa fa una libreria che indovinaCosa fa NextPDF
Una stringa di orientamento come "portait"Ripiega su un valore predefinito ed esegue comunque il renderingaddPage() accetta l’enum Orientation, non una stringa — un refuso è un errore di tipo, non un valore predefinito silenzioso
Un true finale non qualificato passato a cell()Sceglie la posizione booleana che presume voluta dal chiamanteIl booleano è nominato al punto di chiamata (newLine: true); un letterale senza nome è il code smell che l’API elimina
Un wrapper php:// o un percorso di traversal passato a save()«Fa del suo meglio» e scrive da qualche parteViene rifiutato prima che il PDF sia costruito, con un’InvalidConfigException tipizzata che nomina chiave, valore e tipo atteso
setSignature() seguito da save() mentre il writer di alto livello non è collegatoEmette un file non firmato che il chiamante ritiene firmatoLancia NotImplementedException prima di produrre byte, nominando il percorso supportato
Riutilizzare un’istanza Document per un secondo renderingIndovina se lo stato residuo sia ancora applicabileNessun reset() e nessun percorso di riutilizzo — un’istanza fresca per ogni richiesta tramite DocumentFactory, così non c’è alcuno stato residuo da indovinare

L’intento è un argomento obbligatorio. Il contratto centrale, PdfDocumentInterface, accetta geometria e allineamento come oggetti valore tipizzati ed enum, non come primitivi non vincolati:

public function addPage(
?PageSize $size = null,
Orientation $orientation = Orientation::Portrait,
): static;
public function cell(
float $width,
float $height,
string $text = '',
bool|string $border = false,
bool $newLine = false,
Alignment $align = Alignment::Left,
bool $fill = false,
): static;

Orientation e Alignment sono enum, quindi la chiamata non può passare "portait" e farlo interpretare silenziosamente come «predefinito». Dove esiste un valore predefinito, è sicuro (verticale, sinistra, senza bordo), non un’ipotesi su ciò che il chiamante probabilmente desiderava.

I booleani ambigui sono nominati al punto di chiamata. Negli esempi che fungono da riferimento API de facto ricorre la stessa forma:

$document->cell(0, 15, 'Hello, NextPDF!', newLine: true);
$document->setSignature(certInfo: $certInfo, level: SignatureLevel::PAdES_B_B);
$pdf = $document->output(dest: OutputDestination::String);

newLine: true è inequivocabile. Un true finale non qualificato non lo sarebbe. Il livello di firma è SignatureLevel::PAdES_B_B, un caso di enum — mai una stringa che il motore deve interpretare. La destinazione di output è OutputDestination::String, così «dammi i byte, nessun header HTTP, nessun file» è dichiarato, non presunto dalla presenza di un nome di file.

L’input non sicuro è rifiutato prima che venga scritto un solo byte. save() convalida il percorso di destinazione prima di costruire il PDF:

public function save(string $path): void
{
// Reject stream wrappers and null bytes
if (\str_contains($path, "\0") || \preg_match('#^[a-zA-Z]+://#', $path)) {
throw new InvalidConfigException(
configKey: 'output_path',
givenValue: $path,
expectedType: 'valid_path',
);
}
// Resolve the parent directory to prevent path traversal
$dir = \dirname($path);
$realDir = \realpath($dir);
if ($realDir === false) {
throw new InvalidConfigException(
configKey: 'output_path',
givenValue: $dir,
expectedType: 'existing_directory',
);
}
// ... only now is the PDF built and written atomically
}

Il motore non «fa del suo meglio» con un wrapper php:// o un percorso di traversal. Si rifiuta, e l’eccezione nomina la chiave, il valore e ciò che era atteso.

Il motore si rifiuta anziché emettere un artefatto fuorviante. La forma più forte del rifiuto di tirare a indovinare è rinunciare del tutto alla produzione di output quando quell’output sarebbe ingannevole. Quando una firma di alto livello è configurata ma il punto di integrazione del writer che firmerebbe davvero non è collegato, il percorso di build lancia un’eccezione prima di produrre byte, anziché emettere un file non firmato che il chiamante ritiene firmato:

if ($this->padesOrchestrator !== null) {
throw new NotImplementedException(
feature: 'Document::setSignature()->save()/output()/getPdfData()',
followUp: 'The high-level PAdES writer seam is not yet wired ... '
. 'Produce a signed PDF via the direct two-phase '
. 'PadesOrchestrator::signDocument() then finalizeSignature() '
. 'buffer API ...',
);
}

Un PDF non firmato che sembra firmato è esattamente il tipo di artefatto errato ma plausibile che questo principio esiste per prevenire. La stessa posizione compare nel percorso CSS rigoroso. Una deviazione dalla specifica non registrata lancia una StrictModeViolation nel punto di rilevamento, anziché renderizzare un’approssimazione e lasciare la deviazione senza segnalarla.

L’uso singolo elimina un’intera classe di ipotesi. Un Document è a uso singolo — costruito, emesso, scartato. Non esiste alcun reset() e nessun percorso di riutilizzo. Un worker a esecuzione prolungata crea un’istanza fresca per ogni richiesta tramite DocumentFactory. Il motore non deve mai indovinare se lo stato residuo di un documento precedente sia ancora significativo, perché per costruzione non ne esiste alcuno.

Questa pagina è Evidence: Code-backed : ogni forma descritta sopra è citata dal codice sorgente del motore stesso e dai suoi esempi, non dedotta da una parafrasi dell’intento.

  • Le firme tipizzate, con enum, sono il contratto pubblico in PdfDocumentInterface. Lo stile di chiamata con argomenti con nome è la forma coerente in tutti gli esempi canonici che fungono da riferimento API de facto.
  • La convalida del percorso pre-rendering, con la sua InvalidConfigException tipizzata, e la guardia NotImplementedException che rifiuta prima di emettere output sono citate testualmente dal percorso di output della facciata del documento.
  • Il riferimento normativo è Spec: ISO/IEC 25010, §3.32 — la protezione dagli errori dell’utente, la proprietà di qualità che un’API che si rifiuta di tirare a indovinare esiste per soddisfare al punto di chiamata. Il secondo riferimento è Spec: ISO 32000-2, §12.8 , ed è il motivo per cui intervenire per supposizione su un documento firmato non è mai innocuo. Il digest copre un intervallo di byte dichiarato che esclude il valore della firma, quindi qualsiasi riscrittura silenziosa lo invalida.

Un programma piccolo e completo. Ogni riga potenzialmente ambigua dichiara il proprio intento. L’unico input non sicuro viene rifiutato prima che venga costruito un solo byte.

<?php
declare(strict_types=1);
use NextPDF\Contracts\OutputDestination;
use NextPDF\Core\Document;
use NextPDF\Exception\InvalidConfigException;
use NextPDF\ValueObjects\PageSize;
use NextPDF\Contracts\Orientation;
$document = Document::createStandalone();
$document->setTitle('Quarterly Report');
// Intent is explicit: a typed page size and an Orientation enum case,
// not a string the engine has to interpret.
$document->addPage(PageSize::a4(), Orientation::Landscape);
$document->setFont('helvetica', 'B', 16);
// Ambiguous boolean is named, so the call reads as intent.
$document->cell(0, 12, 'Quarterly Report', newLine: true);
try {
// Unsafe path is rejected before a byte is built.
$document->save('php://output/report.pdf');
} catch (InvalidConfigException $e) {
// "Invalid configuration for key "output_path": expected valid_path, ..."
error_log($e->getMessage());
// The String destination is explicit: bytes only, no HTTP headers,
// no file side effect. Nothing is inferred from a missing filename.
$bytes = $document->output(dest: OutputDestination::String);
}

Non esiste alcun percorso in cui questo programma possa fare silenziosamente la cosa sbagliata. O dichiara l’intento e procede, oppure indica il problema e si ferma.

L’obiezione frequente è «questa è solo verbosità». Non è verbosità. È l’assenza di valori predefiniti nascosti. Un true non qualificato è più corto di newLine: true esattamente della quantità di chiarezza che rimuove. Il motore scambia qualche carattere in più al punto di chiamata con l’eliminazione di una categoria di bug — quella in cui il codice compila, viene eseguito, produce un file ed è sbagliato.

Un equivoco correlato è che fail-fast significhi «lancia molte eccezioni». Nell’uso normale NextPDF non lancia nulla. L’input valido fluisce attraverso. Le guardie scattano solo su input genuinamente ambigui o non sicuri — esattamente gli input che è necessario conoscere subito, non quelli da lasciare indovinare al motore.

Il rifiuto di tirare a indovinare si applica all’intento e alla sicurezza, non a ogni comodità. NextPDF ha comunque valori predefiniti sicuri: orientamento verticale, allineamento a sinistra, nessun bordo. Il principio è che un valore predefinito viene offerto solo dove è sicuro e non sorprendente, e mai dove una deduzione errata produce un documento errato.

Questa pagina dimostra il principio sulla superficie API pubblica centrale (la facciata del documento, il suo contratto e il percorso di output). I sottosistemi hanno i propri punti di ingresso, e ciascuno documenta il proprio comportamento di convalida. Le forme qui citate sono aggiornate alla data di questa revisione. Illustrano lo schema; non sono un catalogo esaustivo di ogni guardia presente nel motore.

Le guardie fail-fast descritte sono guardie di correttezza e sicurezza. Non sono, di per sé, un confine di sicurezza. La convalida dell’input è uno strato. La filosofia di progettazione e la documentazione sulla sicurezza descrivono l’impostazione più ampia.

  • Code-backed (livello di evidenza) — una pagina le cui affermazioni sono verificate rispetto al codice sorgente del motore stesso o a un esempio eseguibile, con citazioni anziché parafrasi.
  • Fail fast — rifiutare un input non valido il prima possibile, con una causa chiara, anziché procedere e fallire in modo oscuro più tardi.
  • Argomento con nome — una sintassi PHP al punto di chiamata (newLine: true) che lega un valore a un parametro per nome, rendendo autodescrittivo un letterale altrimenti ambiguo.
  • Ciclo di vita a uso singolo — il contratto Document a uso singolo: istanziare, scrivere, salvare, scartare. Nessun reset(), nessun riutilizzo. I worker creano un’istanza fresca per ogni richiesta tramite DocumentFactory.
  • PAdES — PDF Advanced Electronic Signatures, la famiglia di profili ETSI per la firma di PDF. Espanso al primo utilizzo; trattato in dettaglio nelle pagine sulla firma.