Ir al contenido

Errores como funcionalidad

Spec: ISO 9241-110, §5.6.4 Evidence: Code-backed

NextPDF trata su jerarquía de excepciones como una superficie de API, diseñada con el mismo cuidado que los métodos que las lanzan. Cada fallo es específico, tipado, capturable con la granularidad necesaria y aporta contexto estructurado para los registros.

Esta página muestra esa superficie en el propio código del motor: el tipo base, las subclases tipadas, los constructores con nombre que vinculan una causa raíz con el mensaje y el contexto estructurado que expone cada excepción de NextPDF.

Un mensaje de error es el motor hablando en el peor momento posible: producción, las 2 de la madrugada y un documento que debería haberse entregado. Lo que el mensaje diga en ese momento decide si el siguiente paso es una corrección o una investigación larga.

Un RuntimeException: something went wrong genérico no ofrece ningún punto de partida. Indica que el motor falló, pero no qué falló, ni dónde, y mucho menos qué hacer. La orientación sobre factores humanos es directa al respecto. Un error debería explicarse a sí mismo lo suficiente como para que corregirlo sea el siguiente paso evidente, no un proyecto de investigación ( Spec: ISO 9241-110, §5.6.4.3 ). Una excepción que nombra la causa y el remedio no es un lujo. Es la diferencia entre una corrección de cinco minutos y una de cinco horas.

  • Cada fallo de NextPDF extiende una única base abstracta, NextPdfException, lo que permite capturar todos los errores de la biblioteca con un solo tipo.
  • Por debajo se sitúan subclases específicas y tipadas —una fuente que no se encuentra, una configuración que no es válida, una operación de firma fallida—, para capturar exactamente el fallo que se sabe gestionar.
  • Cada excepción de NextPDF implementa ContextAwareExceptionInterface y expone getContext(): un mapa estructurado y seguro para registros, de modo que no haga falta analizar una cadena de mensaje para recuperar diagnósticos.
  • Los mensajes son accionables: los constructores con nombre vinculan la causa raíz real (y a menudo la corrección) al mensaje, en lugar de una plantilla genérica.
  • Cada clase de excepción documenta quién puede actuar sobre ella —desarrollador, infraestructura o invocador de la biblioteca—, para que la clasificación empiece antes de leer la traza de pila.

La jerarquía es poco profunda y deliberada. Hay una única base, una capa de tipos específicos del dominio y un contrato que todos ellos respetan.

Una única base, captura general por diseño. NextPdfException es abstracta, extiende RuntimeException e implementa ContextAwareExceptionInterface:

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

Que sea abstracta es una decisión deliberada. La base imprecisa no se captura por accidente, porque nunca se lanza directamente. Se captura como red de seguridad, y se captura una subclase específica cuando es posible hacer algo específico.

Subclases específicas y tipadas. Una fuente ausente no es un error genérico; es FontNotFoundException, e incluye los datos necesarios para actuar:

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()
}

El mensaje nombra la fuente y las rutas exactas que se buscaron. No hay que adivinar qué directorio faltaba. La excepción lo indica.

Contexto estructurado, no análisis de cadenas. Cada excepción devuelve un mapa en snake_case, solo con primitivos, seguro para serializar directamente en un registro o una carga útil de APM:

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

El contrato es explícito sobre el motivo. Un middleware de registro puede llamar a $logger->error($e->getMessage(), $e->getContext()) para cualquier excepción de NextPDF sin analizar nunca el mensaje. El mensaje es para las personas. El contexto es para las máquinas. Ninguno tiene que hacer el papel del otro.

Mensajes accionables mediante constructores con nombre. Aquí es donde los errores dejan de ser incidentales y pasan a estar diseñados. SignatureException no solo dice «la firma falló en el nivel B-LT». Ofrece constructores con nombre que vinculan la causa raíz real y, con frecuencia, el remedio exacto, al mensaje:

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

El mensaje indica qué está mal y qué hacer al respecto. Hay constructores hermanos para un paquete de capacidad ausente, un cliente HTTP ausente, un algoritmo de solo resumen elegido por error, un tipo de clave que no coincide con el algoritmo y otros casos. Cada uno convierte una clase de fallo en una frase sobre la que un desarrollador puede actuar sin leer el código del motor.

Fallos que son ruidosos a propósito. Algunas excepciones existen precisamente para convertir una omisión silenciosa en una señal explícita. NotImplementedException lleva una etiqueta feature apta para búsquedas automáticas y una referencia 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,
);
}
}

Una ruta alcanzada pero no conectada lanza esto en lugar de devolver una operación nula plausible. La misma idea sustenta StrictModeViolation, cuyas subclases llevan una etiqueta corta apta para búsqueda del constructo desviado, más contexto opcional de ubicación y de cita. Una desviación de la especificación se convierte en una parada tipada y contextual, no en una representación silenciosamente incorrecta.

Metadatos de clasificación en la propia clase. Cada clase de excepción indica quién puede actuar sobre ella en su docblock. Por ejemplo, FontNotFoundException es «Desarrollador (verificar la ruta de la fuente) o Infraestructura (corregir los permisos de archivo)». InvalidConfigException es «Desarrollador (corregir la configuración antes de llamar a NextPDF)». NotImplementedException es «Invocadores de la biblioteca: o bien eliminar la llamada o bien fijarla a una versión futura». La clasificación empieza antes de la traza de pila, porque la pregunta «¿es mía o de operaciones?» ya tiene una respuesta documentada.

La tabla resume el diseño y lo que cada propiedad le aporta.

Propiedad de diseñoEn el códigoQué aporta
Una única base abstractaNextPdfException (abstracta, implementa la interfaz de contexto)Permite capturar cualquier error de la biblioteca con un solo tipo, nunca la base imprecisa por accidente
Subclases específicas y tipadasFontNotFoundException, InvalidConfigException, SignatureException, …Permite capturar exactamente el fallo que se sabe gestionar
Contexto estructuradogetContext() — solo primitivos en snake_caseRegistrar o enviar a APM sin analizar una cadena de mensaje
Mensajes accionablesLos constructores con nombre vinculan causa raíz + remedioUna frase sobre la que se puede actuar, no una plantilla
Ruidoso a propósitoNotImplementedException, StrictModeViolationUna omisión silenciosa se convierte en una parada tipada y apta para búsqueda
Metadatos de clasificación«Actionable by:» en el docblock de cada claseSaber de quién es el problema antes de leer la traza

Esta página es Evidence: Code-backed : cada clase, firma y forma de mensaje está citada desde el espacio de nombres de excepciones del motor, no parafraseada.

  • La base abstracta y su contrato ContextAwareExceptionInterface, las subclases tipadas, la forma de getContext() y los constructores con nombre de SignatureException están citados literalmente del código.
  • Las líneas de clasificación «Actionable by:» son contratos de docblock de clase en esos mismos archivos.
  • El anclaje en factores humanos es Spec: ISO 9241-110 — §5.6.4.3, sobre errores que se explican lo suficiente como para ser corregidos, y el principio de robustez ante errores de uso del §6. El motor trata al desarrollador como usuario y a la excepción como la interfaz que debe satisfacer esas cláusulas.

Capturar de forma amplia como red de seguridad, de forma específica allí donde sea posible actuar, y entregar el contexto estructurado directamente al registrador: sin analizar mensajes.

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

El catch específico permite recuperarse porque el tipo de excepción indicó que la recuperación era posible. La red de seguridad registra el contexto estructurado para todo lo demás. En ningún momento la aplicación analiza el mensaje para averiguar qué ocurrió.

La interpretación errónea habitual es que un árbol de excepciones profundo es sobreingeniería, y que un único tipo de error sería más simple. Sería más simple para el motor y peor para quien lo integra. Un único tipo significa que cada fallo es una traza de pila genérica y que la lógica de recuperación depende de coincidencias de cadenas. Esa coincidencia es frágil, y la siguiente reformulación del mensaje la rompe. Una jerarquía pequeña y específica traslada ese conocimiento al sistema de tipos, donde el compilador y los bloques catch pueden usarlo.

Un segundo concepto erróneo es que el mensaje y el contexto son redundantes. No lo son. El mensaje es prosa para una persona que lee una línea de registro. El contexto es un mapa tipado para enrutamiento de código, alertas o paneles. Confundirlos es exactamente la trampa de análisis de cadenas que el contrato getContext() existe para eliminar.

La jerarquía es intencionadamente poco profunda. NextPDF no crea una clase de excepción distinta para cada fallo concebible. La crea cuando capturar ese fallo específicamente es algo que un invocador haría razonablemente. Dividir en exceso cambiaría el problema del análisis de cadenas por un problema de lista de capturas inmanejable.

getContext() está estructurado para registros y APM, así que devuelve solo primitivos y listas de primitivos, sin objetos anidados, por contrato. Es contexto de diagnóstico, no una instantánea serializada del estado interno del motor. Tampoco es un formato de intercambio estable sobre el que construir esquemas externos.

Esta página describe la superficie de diseño de las excepciones. El conjunto exacto de excepciones y sus campos evoluciona con el motor. Las clases y formas citadas aquí están vigentes en la fecha de esta revisión y son ilustrativas del contrato, no un catálogo congelado. El contrato —una única base, subclases tipadas, contexto estructurado, mensajes accionables— es la parte estable.

  • Respaldado por código (nivel de evidencia) — una página cuyas afirmaciones se verifican contra el propio código del motor, citado en lugar de parafraseado.
  • Excepción consciente del contexto — una excepción de NextPDF que implementa ContextAwareExceptionInterface y expone getContext(). Ese método devuelve un mapa en snake_case de campos de diagnóstico primitivos, seguro para serializar en un registro o una carga útil de APM sin analizar la cadena del mensaje.
  • Constructor con nombre — un método de fábrica estático (por ejemplo SignatureException::tsaUrlEmpty()) que construye una excepción con un mensaje vinculado a una causa raíz específica y, a menudo, a su remedio.
  • PAdES — Firmas Electrónicas Avanzadas para PDF, la familia de perfiles de ETSI para la firma de PDF. El acrónimo se desarrolla en su primer uso; se trata en profundidad en las páginas de firma.
  • TSA — Autoridad de Sellado de Tiempo, el servicio de confianza que emite las marcas de tiempo RFC 3161 que utilizan los perfiles PAdES superiores.