Ir al contenido

Una API que se niega a adivinar

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

NextPDF obliga a declarar exactamente la intención. Allí donde la intención cambia los bytes —un nivel de firma, un destino de salida, un objetivo de conformidad— es un argumento obligatorio y explícito, no algo que el motor infiera del contexto.

Esta página muestra esa postura en el propio código fuente del motor: las firmas de los métodos, los argumentos con nombre y los puntos donde una entrada ambigua se rechaza antes de producir un solo byte.

Una conjetura es una decisión tomada en nombre de quien llama sin avisar. Para un campo de texto eso resulta apenas molesto. Para un PDF es un defecto latente, porque lo que se entrega suele ser un artefacto legal o de archivo cuya corrección se comprueba más tarde, por otra persona, con un validador.

Considérese una firma. Su resumen se calcula sobre un rango de bytes declarado que excluye deliberadamente el propio valor de la firma ( Spec: ISO 32000-2, §12.8 ). Una API que «ayuda» en silencio —reescribiendo la estructura, infiriendo un nivel, rellenando un marcador de posición— no ha ayudado. Ha cambiado los bytes que una firma debía proteger. La conjetura que parece amable en el punto de llamada termina siendo el incidente de producción semanas después. Son la misma línea de código.

  • Si una elección cambia la salida y no tiene un valor por defecto seguro, NextPDF la convierte en un argumento obligatorio, no en uno inferido.
  • Los argumentos opcionales que podrían leerse de forma ambigua llevan nombre, de modo que el punto de llamada declara la intención (newLine: true, no un simple true).
  • Las entradas que podrían ser inseguras se validan antes del renderizado y se rechazan con una excepción tipada que nombra la causa.
  • Una instancia de documento es de un solo uso: se construye, se emite y se descarta. No hay reset(), así que no hay «¿se está reutilizando esto?» que adivinar.
  • El motor nunca emite un artefacto de apariencia plausible en lugar del que se ha pedido. Lo rechaza.

El mecanismo no es vistoso, y ese es justo el punto. Es el sistema de tipos, los argumentos con nombre, las enumeraciones en lugar de cadenas mágicas y un pequeño número de cláusulas de guarda deliberadas situadas antes de la salida.

La tabla contrasta algunas entradas ambiguas. Para cada una muestra lo que inferiría una biblioteca que «ayuda» y lo que hace NextPDF en su lugar. Cada columna de NextPDF es un comportamiento citado del código fuente que se muestra más adelante en esta página.

Entrada ambiguaLo que hace una biblioteca que adivinaLo que hace NextPDF
Una cadena de orientación como "portait"Recurre a un valor por defecto y renderiza de todos modosaddPage() recibe la enumeración Orientation, no una cadena: un error tipográfico es un error de tipo, no un valor por defecto silencioso
Un true suelto al final de cell()Elige la posición booleana que supone que se queríaEl booleano lleva nombre en el punto de llamada (newLine: true); un literal sin nombre es justo el indicio que la API elimina
Un envoltorio php:// o una ruta de traversía hacia save()«Hace lo que puede» y escribe en algún sitioRechazada antes de construir el PDF, con una InvalidConfigException tipada que nombra la clave, el valor y el tipo esperado
setSignature() y luego save() mientras el firmante de alto nivel no está conectadoEmite un archivo sin firmar que quien llama cree firmadoLanza NotImplementedException antes de producir bytes, nombrando la ruta admitida
Reutilizar una instancia de Document para un segundo renderizadoAdivina si el estado residual sigue aplicándoseSin reset() ni ruta de reutilización: una instancia nueva por petición mediante DocumentFactory, de modo que no hay estado residual sobre el que adivinar

La intención es un argumento obligatorio. El contrato central, PdfDocumentInterface, recibe la geometría y la alineación como objetos de valor tipados y enumeraciones, no como primitivos sueltos:

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 y Alignment son enumeraciones, de modo que no se puede pasar "portait" y hacer que signifique en silencio «por defecto». Donde existe un valor por defecto, es uno seguro (vertical, izquierda, sin borde), no una conjetura sobre lo que probablemente se pretendía.

Los booleanos ambiguos llevan nombre en el punto de llamada. A lo largo de los ejemplos que sirven como referencia de facto de la API, se repite la misma 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 es inconfundible. Un true suelto al final no lo sería. El nivel de firma es SignatureLevel::PAdES_B_B, un caso de enumeración, nunca una cadena que el motor tenga que interpretar. El destino de salida es OutputDestination::String, de modo que «dame los bytes, sin cabeceras HTTP, sin archivo» se declara; no se asume según si se pasó o no un nombre de archivo.

La entrada insegura se rechaza antes de escribir un solo byte. save() valida la ruta de destino antes de construir el 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
}

El motor no «hace lo que puede» con un envoltorio php:// ni con una ruta de traversía. Se niega, y la excepción nombra la clave, el valor y lo que se esperaba.

El motor se niega en lugar de emitir un artefacto engañoso. La forma más fuerte de negarse a adivinar es no producir salida alguna cuando esa salida sería falsa. Cuando se configura una firma de alto nivel pero el punto de integración del escritor que la firmaría realmente no está conectado, la ruta de construcción lanza antes de producir bytes, en lugar de emitir un archivo sin firmar que quien llama cree firmado:

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 sin firmar que parece firmado es justo el tipo de artefacto erróneo de apariencia plausible que este principio existe para evitar. La misma postura aparece en la ruta estricta de CSS. Una desviación de la especificación no registrada lanza una StrictModeViolation en el punto de detección, en lugar de renderizar una aproximación y dejar la desviación sin detectar.

El uso único elimina toda una clase de conjeturas. Un Document es desechable: se construye, se emite y se descarta. No hay reset() ni ruta de reutilización. Un worker de larga duración crea una instancia nueva por petición mediante DocumentFactory. El motor nunca tiene que adivinar si el estado residual de un documento anterior sigue siendo significativo, porque por construcción no hay ninguno.

Esta página es Evidence: Code-backed : cada patrón anterior está citado del propio código fuente del motor y de sus ejemplos, no inferido a partir de la intención.

  • Las firmas tipadas que usan enumeraciones son el contrato público de PdfDocumentInterface. El estilo de llamada con argumentos con nombre es la forma coherente en todos los ejemplos canónicos que actúan como referencia de facto de la API.
  • La validación de la ruta previa al renderizado, con su InvalidConfigException tipada, y la guarda NotImplementedException que se niega antes de emitir están citadas textualmente de la ruta de salida de la fachada del documento.
  • El anclaje normativo es Spec: ISO/IEC 25010, §3.32 : protección frente a errores del usuario, la propiedad de calidad que una API que se niega a adivinar existe para satisfacer en el punto de llamada. El segundo anclaje es Spec: ISO 32000-2, §12.8 , que es la razón por la que hacer conjeturas sobre un documento firmado nunca es inofensivo. El resumen cubre un rango de bytes declarado que excluye el valor de la firma, de modo que cualquier reescritura silenciosa lo invalida.

Un programa pequeño y completo. Cada línea que podría ser ambigua declara su intención. La única entrada insegura se rechaza antes de hacer cualquier trabajo.

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

No existe ninguna ruta en la que este programa haga lo incorrecto en silencio. O bien declara la intención y continúa, o bien nombra el problema y se detiene.

La objeción frecuente es «esto no es más que verbosidad». No es verbosidad. Es la ausencia de valores por defecto ocultos. Un true suelto es más corto que newLine: true exactamente en la cantidad de claridad que elimina. El motor cambia unos pocos caracteres en el punto de llamada por la eliminación de una categoría de error: aquella en la que el código compila, se ejecuta, produce un archivo y es incorrecto.

Un concepto erróneo relacionado es que fallar rápido significa «lanza mucho». En el uso normal, NextPDF no lanza excepciones. La entrada válida fluye sin obstáculos. Las guardas se activan solo ante entradas genuinamente ambiguas o inseguras: precisamente las entradas que conviene detectar de inmediato, no las que conviene que se adivinen.

Negarse a adivinar se aplica a la intención y la seguridad, no a toda comodidad. NextPDF sigue teniendo valores por defecto seguros: orientación vertical, alineación a la izquierda, sin borde. El principio es que un valor por defecto solo se ofrece allí donde es seguro y previsible, y nunca donde la inferencia equivocada produce un documento equivocado.

Esta página demuestra el principio sobre la superficie pública central de la API (la fachada del documento, su contrato y la ruta de salida). Los subsistemas tienen sus propios puntos de entrada, y cada uno documenta su propio comportamiento de validación. Las formas citadas aquí están vigentes en la fecha de esta revisión. Ilustran el patrón; no son un catálogo exhaustivo de cada guarda del motor.

Las guardas de fallo rápido descritas son guardas de corrección y seguridad. No son una frontera de seguridad por sí solas. La validación de entrada es una capa. La filosofía de diseño y la documentación de seguridad describen la postura más amplia.

  • Respaldado por código (nivel de evidencia): una página cuyas afirmaciones se verifican con el propio código fuente del motor o con un ejemplo ejecutable, citado en lugar de parafraseado.
  • Fallo rápido: rechazar una entrada no válida en el punto más temprano, con una causa clara, en lugar de continuar y fallar de forma oscura más adelante.
  • Argumento con nombre: una sintaxis de PHP en el punto de llamada (newLine: true) que vincula un valor a un parámetro por su nombre, haciendo que un literal de otro modo ambiguo quede autodescrito.
  • Ciclo de vida de un solo uso: el contrato desechable de Document: instanciar, escribir, guardar, descartar. Sin reset(), sin reutilización. Los workers crean una instancia nueva por petición mediante DocumentFactory.
  • PAdES: PDF Advanced Electronic Signatures, la familia de perfiles ETSI para la firma de PDF. Se desarrolla en su primer uso; se trata en profundidad en las páginas de firma.