Aller au contenu

Une API qui refuse de deviner

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

NextPDF t’oblige à exprimer clairement ce que tu veux. Dès que l’intention modifie les octets — niveau de signature, destination de sortie, cible de conformité — elle devient un argument explicite et obligatoire, pas un élément que le moteur déduit du contexte.

Cette page illustre cette posture dans le code source du moteur lui-même : signatures de méthode, arguments nommés et points où une entrée ambiguë est rejetée avant que le moindre octet ne soit produit.

Une supposition, c’est une décision prise à ta place sans te le dire. Dans un champ de texte, c’est légèrement agaçant. Dans un PDF, c’est un défaut latent, car ce que tu livres est souvent un artefact à valeur légale ou d’archivage dont la justesse est vérifiée plus tard, par quelqu’un d’autre, avec un validateur.

Prends le cas d’une signature. Son condensat est calculé sur une plage d’octets déclarée qui exclut délibérément la valeur de signature elle-même ( Spec: ISO 32000-2, §12.8 ). Une API qui « aide » discrètement — en réécrivant la structure, en déduisant un niveau, en complétant un emplacement réservé — n’a pas aidé. Elle a modifié les octets qu’une signature était censée protéger. La supposition qui semble bienveillante au site d’appel devient l’incident de production plusieurs semaines plus tard. C’est la même ligne de code.

  • Si un choix change la sortie et n’a pas de valeur par défaut sûre, NextPDF en fait un argument obligatoire, pas un argument déduit.
  • Les arguments optionnels dont la lecture serait ambiguë sont nommés, pour que le site d’appel déclare l’intention (newLine: true, et non un simple true).
  • Les entrées potentiellement dangereuses sont validées avant le rendu, puis rejetées avec une exception typée qui nomme la cause.
  • Une instance de document est à usage unique : elle est construite, émise, puis abandonnée. Il n’y a pas de reset(), donc aucun « cet objet est-il réutilisé ? » à deviner.
  • Le moteur n’émet jamais un artefact d’apparence plausible à la place de celui que tu as demandé. Il refuse tout simplement.

Le mécanisme n’a rien de spectaculaire, et c’est précisément le but. Il repose sur le système de types, les arguments nommés, des énumérations plutôt que des chaînes magiques, et quelques clauses de garde délibérées placées avant la sortie.

Le tableau compare quelques entrées ambiguës. Pour chacune, il montre ce qu’une bibliothèque qui « aide » déduirait à ta place, et ce que NextPDF fait à la place. Dans la colonne NextPDF, chaque comportement est cité depuis le code source présenté plus loin sur cette page.

Entrée ambiguëCe que ferait une bibliothèque qui devineCe que fait NextPDF
Une chaîne d’orientation comme "portait"Se rabat sur une valeur par défaut et effectue le rendu quand mêmeaddPage() prend l’énumération Orientation, pas une chaîne — une faute de frappe est une erreur de type, pas une valeur par défaut silencieuse
Un simple true en fin d’appel à cell()Interprète le booléen comme le paramètre qu’elle suppose viséLe booléen est nommé sur le site d’appel (newLine: true) ; un littéral sans nom est l’odeur de code que l’API supprime
Un wrapper php:// ou un chemin de traversée transmis à save()« Fait de son mieux » et écrit quelque partRejeté avant la construction du PDF, avec une InvalidConfigException typée nommant la clé, la valeur et le type attendu
setSignature() puis save() alors que le signataire de haut niveau n’est pas câbléÉmet un fichier non signé que l’appelant croit signéLève NotImplementedException avant de produire des octets, en nommant le chemin pris en charge
Réutiliser une instance de Document pour un second renduDevine si l’état résiduel doit encore s’appliquerAucun reset() et aucune voie de réutilisation — une instance neuve par requête via DocumentFactory, donc aucun état résiduel à deviner

L’intention est un argument obligatoire. Le contrat principal, PdfDocumentInterface, reçoit la géométrie et l’alignement sous forme d’objets-valeurs typés et d’énumérations, pas de primitives permissives :

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 et Alignment sont des énumérations : l’appel ne peut donc pas passer "portait" et lui donner silencieusement le sens de « valeur par défaut ». Là où une valeur par défaut existe, c’est une valeur sûre (portrait, à gauche, sans bordure), pas une supposition sur ce que tu voulais probablement.

Les booléens ambigus sont nommés sur le site d’appel. Dans les exemples qui font office de référence d’API de facto, la même forme revient :

$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 est sans équivoque. Un simple true en fin d’appel ne le serait pas. Le niveau de signature est SignatureLevel::PAdES_B_B, un cas d’énumération — jamais une chaîne que le moteur doit interpréter. La destination de sortie est OutputDestination::String, donc l’intention « donne-moi les octets, sans en-têtes HTTP, sans fichier » est déclarée, et non déduite de la présence ou de l’absence d’un nom de fichier.

Les entrées dangereuses sont rejetées avant qu’un seul octet ne soit écrit. save() valide le chemin de destination avant de construire le 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
}

Le moteur ne « fait pas de son mieux » avec un wrapper php:// ou un chemin de traversée. Il refuse, et l’exception nomme la clé, la valeur et le type attendu.

Le moteur refuse plutôt que d’émettre un artefact trompeur. La forme la plus nette du refus de deviner consiste à ne pas produire de sortie du tout lorsque cette sortie serait mensongère. Quand une signature de haut niveau est configurée mais que le point d’intégration du writer censé signer réellement n’est pas câblé, le chemin de construction lève une exception avant de produire des octets, au lieu d’émettre un fichier non signé que l’appelant croit signé :

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 signé qui semble signé est précisément le type d’artefact erroné mais plausible que ce principe vise à éviter. La même posture apparaît dans la voie CSS stricte. Tout écart de spécification non enregistré lève une StrictModeViolation dès sa détection, plutôt que de rendre une approximation et de laisser l’écart passer inaperçu.

L’usage unique élimine toute une catégorie de suppositions. Un Document est jetable — construit, émis, abandonné. Il n’y a pas de reset() ni de voie de réutilisation. Un worker de longue durée crée une instance neuve par requête au moyen de DocumentFactory. Le moteur n’a jamais à deviner si l’état résiduel d’un document précédent a encore un sens, puisqu’il n’en existe aucun par construction.

Cette page est Evidence: Code-backed  : chaque forme ci-dessus est tirée du code source du moteur lui-même et de ses exemples, pas paraphrasée à partir de l’intention.

  • Les signatures typées qui portent des énumérations sont le contrat public dans PdfDocumentInterface. Le style avec arguments nommés revient constamment dans les exemples canoniques qui font office de référence d’API de facto.
  • La validation avant rendu du chemin, avec sa InvalidConfigException typée, et la garde NotImplementedException qui refuse avant émission sont citées verbatim depuis le chemin de sortie de la façade du document.
  • L’ancrage normatif est Spec: ISO/IEC 25010, §3.32 — la protection contre les erreurs d’utilisateur, la propriété de qualité qu’une API qui refuse de deviner vise à satisfaire au site d’appel. Le second ancrage est Spec: ISO 32000-2, §12.8 , ce qui explique pourquoi deviner autour d’un document signé n’est jamais anodin. Le condensat couvre une plage d’octets déclarée qui exclut la valeur de signature, donc toute réécriture silencieuse l’invalide.

Un petit programme complet. Chaque ligne qui pourrait être ambiguë déclare son intention. La seule entrée dangereuse est refusée avant qu’aucun travail ne soit fait.

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

Il n’existe aucun chemin où ce programme fait discrètement la mauvaise chose. Soit il déclare son intention et continue, soit il nomme le problème et s’arrête.

L’objection fréquente est « ce n’est que de la verbosité ». Ce n’est pas de la verbosité. C’est l’absence de valeurs par défaut cachées. Un simple true est plus court que newLine: true d’exactement la quantité de clarté qu’il retire. Le moteur échange quelques caractères au site d’appel contre l’élimination d’une catégorie de bug — celle où le code compile, s’exécute, produit un fichier, mais reste faux.

Une idée fausse connexe est que « échouer vite » signifie « lever beaucoup d’exceptions ». En usage normal, NextPDF ne lève rien. Une entrée valide passe. Les gardes ne se déclenchent que sur des entrées réellement ambiguës ou dangereuses — précisément celles dont tu veux être averti immédiatement, pas celles que tu veux voir devinées.

Le refus de deviner s’applique à l’intention et à la sécurité, pas à chaque commodité. NextPDF conserve tout de même des valeurs par défaut sûres : orientation portrait, alignement à gauche, sans bordure. Le principe est qu’une valeur par défaut n’est offerte que là où elle est sûre et sans surprise, et jamais là où la mauvaise inférence produit un document erroné.

Cette page démontre le principe sur la surface d’API publique principale (la façade du document, son contrat et le chemin de sortie). Les sous-systèmes ont leurs propres points d’entrée, et chacun documente son propre comportement de validation. Les formes citées ici sont à jour à la date de cette revue. Elles illustrent le motif ; elles ne constituent pas un catalogue exhaustif de chaque garde du moteur.

Les gardes d’échec précoce décrites ici sont des gardes de justesse et de sécurité. Elles ne constituent pas une frontière de sécurité à elles seules. La validation des entrées est une couche. La philosophie de conception et la documentation de sécurité décrivent la posture plus large.

  • Adossé au code (niveau de preuve) — une page dont les affirmations sont vérifiées contre le code source du moteur lui-même ou un exemple exécutable, et citées plutôt que paraphrasées.
  • Échouer vite — rejeter une entrée invalide au point le plus précoce, avec une cause claire, au lieu de continuer et d’échouer de manière obscure plus tard.
  • Argument nommé — une syntaxe de site d’appel PHP (newLine: true) qui lie une valeur à un paramètre par son nom, rendant un littéral autrement ambigu auto-descriptif.
  • Cycle de vie à usage unique — le contrat jetable de Document : instancier, écrire, sauvegarder, abandonner. Aucun reset(), aucune réutilisation. Les workers créent une instance neuve par requête au moyen de DocumentFactory.
  • PAdES — PDF Advanced Electronic Signatures, la famille de profils ETSI pour la signature de PDF. Explicité à sa première occurrence ; couvert en profondeur sur les pages de signature.