An API that refuses to guess
Spec: ISO/IEC 25010 ISO/IEC 25010 Spec: ISO 32000-2 ISO 32000-2 Evidence: Code-backed
At a glance
Section titled “At a glance”NextPDF makes you say what you mean. Where intent changes the bytes — a signature level, an output destination, a conformance target — it is a required explicit argument, not something the engine infers from context.
This page shows that stance in the engine’s own source: the method signatures, the named arguments, and the points where an ambiguous input is rejected before any byte is produced.
Why this matters
Section titled “Why this matters”A guess is a decision made on your behalf without telling you. For a text field, that is mildly annoying. For a PDF, it is a latent defect, because what you ship is often a legal or archival artifact whose correctness is checked later by someone else with a validator.
Consider a signature. Its digest is computed over a declared byte range that deliberately excludes the signature value itself ( Spec: ISO 32000-2, §12.8 ISO 32000-2 §12.8 ). An API that quietly “helps” — rewriting structure, inferring a level, padding a placeholder — has not helped. It has changed the bytes a signature was supposed to protect. The guess that seems friendly at the call site becomes the production incident weeks later. They are the same line of code.
The short version
Section titled “The short version”- If a choice changes the output and has no safe default, NextPDF makes it a required argument, not an inferred one.
- Optional arguments that read ambiguously are named, so the call site
states intent (
newLine: true, not a baretrue). - Inputs that could be unsafe are validated before rendering, and rejected with a typed exception that names the cause.
- A document instance is use-once: it is built, emitted, and discarded.
There is no
reset(), so there is no “is this thing reused?” guessing. - The engine never emits a plausible-looking artifact in place of the one you asked for. It refuses instead.
How NextPDF approaches it
Section titled “How NextPDF approaches it”The mechanism is plain, and that is the point. It is the type system, named arguments, enums instead of magic strings, and a small number of deliberate guard clauses placed before output.
The table contrasts a few ambiguous inputs. For each one, it shows what a library that “helps” would infer, and what NextPDF does instead. Every NextPDF column is a behaviour quoted from the source shown later on this page.
| Ambiguous input | What a guessing library does | What NextPDF does |
|---|---|---|
An orientation string like "portait" | Falls back to a default and renders anyway | addPage() takes the Orientation enum, not a string — a typo is a type error, not a silent default |
A bare trailing true to cell() | Picks whichever boolean position it assumes you meant | The boolean is named at the call site (newLine: true); an unnamed literal is the smell the API removes |
A php:// wrapper or traversal path to save() | ”Tries its best” and writes somewhere | Rejected before the PDF is built, with a typed InvalidConfigException naming key, value, and expected type |
setSignature() then save() while the high-level signer is unwired | Emits an unsigned file the caller believes is signed | Throws NotImplementedException before producing bytes, naming the supported route |
Reusing a Document instance for a second render | Guesses whether residual state still applies | No reset() and no reuse path — a fresh instance per request via DocumentFactory, so there is no residual state to guess about |
Intent is a required argument. The core contract,
PdfDocumentInterface, takes geometry and alignment as typed value objects
and enums, not loose primitives:
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 and Alignment are enums, so the call cannot pass "portait"
and have it silently mean “default”. Where a default exists, it is a safe
one (portrait, left, no border), not a guess about what you probably wanted.
Ambiguous booleans are named at the call site. Across the examples that serve as the de-facto API reference, the same shape recurs:
$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 is unmistakable. A bare trailing true would not be. The
signature level is SignatureLevel::PAdES_B_B, an enum case — never a string
the engine has to interpret. The output destination is
OutputDestination::String, so “give me the bytes, no HTTP headers, no file”
is stated. It is not inferred from whether a filename was passed.
Unsafe input is rejected before a byte is written. save() validates
the destination path before it builds the 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}The engine does not “try its best” with a php:// wrapper or a traversal
path. It refuses, and the exception names the key, the value, and what was
expected.
The engine refuses rather than emit a misleading artifact. The strongest form of refusing to guess is declining to produce output at all when that output would be untruthful. When a high-level signature is configured but the writer seam that would actually sign is not wired, the build path throws before producing bytes, instead of emitting an unsigned file the caller believes is signed:
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 ...', );}An unsigned PDF that looks signed is the precise kind of plausible-looking
wrong artifact this principle exists to prevent. The same stance appears in
the strict CSS path. An unregistered spec deviation throws a
StrictModeViolation at the point of detection, rather than rendering an
approximation and leaving the deviation undetected.
Use-once removes a whole class of guesses. A Document is disposable —
built, emitted, and discarded. There is no reset() and no reuse path. A
long-running worker creates a fresh instance per request through
DocumentFactory. The engine never has to guess whether residual state from a
previous document is still meaningful, because there is none by construction.
What the evidence says
Section titled “What the evidence says”This page is Evidence: Code-backed : every shape above is quoted from the engine’s own source and its examples, not paraphrased from intent.
- The typed, enum-bearing signatures are the public contract in
PdfDocumentInterface. The named-argument call style is the consistent form across the canonical examples that act as the de-facto API reference. - The pre-render path validation, with its typed
InvalidConfigException, and the refuse-before-emitNotImplementedExceptionguard are quoted verbatim from the output path of the document façade. - The standards anchor is Spec: ISO/IEC 25010, §3.32 ISO/IEC 25010 §3.32 — user error protection, the quality property a refuse-to-guess API exists to satisfy at the call site. The second anchor is Spec: ISO 32000-2, §12.8 ISO 32000-2 §12.8 , which is why guessing around a signed document is never harmless. The digest covers a declared byte range that excludes the signature value, so any silent rewrite invalidates it.
Practical example
Section titled “Practical example”A small, complete program follows. Every line that could be ambiguous states its intent. The one unsafe input is refused before any work is done.
<?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);}There is no path where this program quietly does the wrong thing. It states intent and proceeds, or it names the problem and stops.
Common misconception
Section titled “Common misconception”The frequent objection is “this is just verbosity”. It is not verbosity. It
is the absence of hidden defaults. A bare true is shorter than
newLine: true by exactly the amount of clarity it removes. The engine
trades a few characters at the call site for the elimination of a category
of bug — the one where the code compiles, runs, produces a file, and is
wrong.
A related misconception is that fail-fast means “throws a lot”. In normal use NextPDF throws nothing. Valid input flows through. The guards fire only on inputs that are genuinely ambiguous or unsafe — precisely the inputs you want to hear about immediately, not the ones you want guessed.
Limits and boundaries
Section titled “Limits and boundaries”Refusing to guess applies to intent and safety, not to every convenience. NextPDF still has safe defaults: portrait orientation, left alignment, no border. The principle is that a default is offered only where it is safe and unsurprising, and never where the wrong inference produces a wrong document.
This page demonstrates the principle on the core public API surface (the document façade, its contract, and the output path). Subsystems have their own entry points, and each documents its own validation behaviour. The shapes quoted here are current as of this review. They illustrate the pattern; they are not an exhaustive catalogue of every guard in the engine.
The fail-fast guards described are correctness and safety guards. They are not a security boundary on their own. Input validation is one layer. The design philosophy and the security documentation describe the wider stance.
Related docs
Section titled “Related docs”- The NextPDF design philosophy — the principle this page demonstrates, in its priority context.
- Errors as a feature — what the typed exceptions these guards throw are designed to tell you.
- Strict types, everywhere — how the type system makes “state your intent” enforceable rather than advisory.
Glossary
Section titled “Glossary”- Code-backed (evidence level) — a page whose claims are checked against the engine’s own source or a runnable example, quoted rather than paraphrased.
- Fail fast — rejecting an invalid input at the earliest point, with a clear cause, instead of proceeding and failing obscurely later.
- Named argument — a PHP call-site syntax (
newLine: true) that binds a value to a parameter by name, making an otherwise ambiguous literal self-describing. - Use-once lifecycle — the disposable
Documentcontract: instantiate, write, save, discard. Noreset(), no reuse. Workers create a fresh instance per request throughDocumentFactory. - PAdES — PDF Advanced Electronic Signatures, the ETSI profile family for PDF signing. Expanded on first use; covered in depth on the signing pages.