Contratos / Streaming
De un vistazo
Sección titulada «De un vistazo»El dominio de streaming reúne dos interfaces experimental: StreamingWriterInterface, para la salida incremental de PDF, y CursorInterface, para la composición de contenido a nivel de página. Core incluye un motor final y probado que implementa ambas. Las clases del motor son internas; por tanto, el comportamiento se consume a través del contrato público experimental, en lugar de implementarlo directamente. Dado que el nivel es experimental, el contrato puede cambiar en una versión menor con aviso previo de obsolescencia. Antes de depender de él en producción, fijar la versión de forma estricta o encapsularlo detrás de un adaptador propio.
Instalación
Sección titulada «Instalación»composer require nextpdf/core:^3Visión general conceptual
Sección titulada «Visión general conceptual»Un escritor de streaming serializa cada página mientras se compone y puede volcarla a la salida antes de que se inicie la página siguiente. Es la vía prevista para una carga de trabajo cuyo documento supera el presupuesto de memoria disponible. El escritor en memoria retiene el documento completo; un escritor de streaming no. StreamingWriterInterface define una máquina de estados estricta. Una instancia recién creada está CLOSED. open() la mueve a OPEN y escribe la cabecera del PDF en un flujo proporcionado por el llamador. newPage() la mueve a PAGING y devuelve un cursor. close() escribe la estructura de referencias cruzadas y el tráiler, y la mueve a un estado terminal CLOSED. Un flujo de referencias cruzadas asigna a cada número de objeto su desplazamiento en bytes — ISO 32000-2 §7. Cada instancia admite una única sesión. Después de close(), la instancia queda agotada. El llamador es propietario del recurso de flujo. El escritor escribe en él, pero nunca lo cierra.
CursorInterface es la superficie de escritura a nivel de página. Un cursor se obtiene de StreamingWriterInterface::newPage() y es válido hasta que se finaliza, hasta que la siguiente newPage() lo finaliza automáticamente o hasta que close() lo invalida. La invalidación es permanente: un cursor no puede reactivarse. Cualquier método invocado sobre un cursor invalidado lanza LogicException. El cursor escribe operadores de flujo de contenido en crudo, establece la fuente activa y escribe texto en una posición determinada. Un flujo de contenido codifica el contenido de la página como una secuencia de operadores gráficos — ISO 32000-2 §8. El cursor es una superficie de bajo nivel: no realiza modelado de texto, reordenación bidireccional, saltos de línea ni ningún tipo de maquetación. Esas tareas siguen siendo responsabilidades del nivel Document. La invariante de cursor único se mantiene en todo momento: como máximo un cursor es válido en cualquier instante.
Ambas interfaces son experimental, y Core incluye un motor operativo detrás de ellas: una implementación final de StreamingWriterInterface, su cursor de página y un sumidero de descarte usado para la evaluación comparativa de memoria. Estas clases del motor son internas y no forman parte de la superficie pública. La forma admitida de usar el streaming consiste en depender del contrato experimental y dejar que Core proporcione la implementación. El PHPDoc de cada tipo remite al ADR del escritor de streaming para la máquina de estados del ciclo de vida y la justificación del alcance. Dado que el nivel es experimental, la firma del contrato aún puede cambiar en una versión menor con aviso previo de obsolescencia. Antes de depender de él en producción, fijar la versión de forma estricta o encapsularlo detrás de un adaptador propio.
Superficie de la API
Sección titulada «Superficie de la API»| Tipo | Clase | Miembros clave | Estabilidad | Desde |
|---|---|---|---|---|
StreamingWriterInterface | interface | open(resource, Config), newPage(?PageSize): CursorInterface, close() | experimental (motor entregado) | 3.1.0 |
CursorInterface | interface | writeContent(string), setFont(string, string, float), writeText(float, float, string), finalizePage() | experimental (motor entregado) | 3.1.0 |
open() lanza InvalidArgumentException cuando el flujo no es escribible y LogicException si el escritor ya está abierto. close() no es idempotente: un doble cierre lanza una excepción.
Ejemplo de código — Inicio rápido
Sección titulada «Ejemplo de código — Inicio rápido»<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\StreamingWriterInterface;use NextPDF\Core\Config;
/** * Drive a streaming writer through one page. * * The parameter is the experimental contract; Core supplies the * implementation. Type-hint the interface and let the engine satisfy it. * * @param StreamingWriterInterface $writer A Core-supplied streaming writer. * @param resource $stream A writable, caller-owned stream. */function writeOnePage(StreamingWriterInterface $writer, $stream): void{ $writer->open($stream, new Config()); $cursor = $writer->newPage(); $cursor->setFont('helvetica', '', 12.0); $cursor->writeText(72.0, 720.0, 'Streamed page.'); $cursor->finalizePage(); $writer->close(); // The caller closes $stream after close() returns.}La función está escrita contra la interfaz experimental, por lo que permanece desacoplada de la clase del motor. Core inyecta una implementación operativa en el punto de llamada.
Ejemplo de código — Producción
Sección titulada «Ejemplo de código — Producción»<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\StreamingWriterInterface;use NextPDF\Core\Config;use NextPDF\ValueObjects\PageSize;use Psr\Log\LoggerInterface;
final readonly class LargeReportStreamer{ public function __construct( private StreamingWriterInterface $writer, private LoggerInterface $logger, ) {}
/** * Stream a multi-page report to a caller-owned file handle. * * @param resource $stream Writable file handle owned by the caller. * @param list<list<string>> $pages One list of text lines per page. */ public function stream($stream, array $pages): void { $this->writer->open($stream, new Config());
try { foreach ($pages as $lines) { $cursor = $this->writer->newPage(PageSize::A4()); $cursor->setFont('helvetica', '', 11.0);
$y = 760.0; foreach ($lines as $line) { $cursor->writeText(72.0, $y, $line); $y -= 14.0; }
$cursor->finalizePage(); } } finally { $this->writer->close(); } }}El bloque finally garantiza que el escritor se cierre y que el tráiler se escriba incluso si el bucle de páginas lanza una excepción. El flujo sigue siendo propiedad del llamador, que se encarga de cerrarlo.
Casos límite y trampas
Sección titulada «Casos límite y trampas»- Depender de la interfaz, no de la clase del motor. El motor que implementa ambos contratos es interno y no forma parte de la superficie pública. No instanciarlo con
newni hacer referencia a él por su nombre. Declarar el tipoStreamingWriterInterfacey dejar que Core proporcione la implementación. - El contrato es
experimental. Su firma puede cambiar en una versión menor, con aviso previo de obsolescencia. Antes de depender de él en producción, fijar la versión de forma estricta o encapsularlo detrás de un adaptador propio. - Un cursor se invalida en el momento en que se llama a la siguiente
newPage()o aclose(). Conservar un cursor obsoleto y llamar a un método sobre él lanzaLogicException. Finalizarlo explícitamente mejora la claridad. close()no es idempotente. Un doble cierre es un error del llamador, no una condición recuperable. El contrato lanza una excepción.- El escritor nunca cierra el flujo. Olvidar cerrar un manejador propiedad del llamador después de que
close()retorne provoca una fuga de descriptor de archivo. - El motor vuelca cada página finalizada para que la memoria residente no crezca con el número de páginas. El perfil exacto de memoria es una propiedad del nivel
experimentaly puede cambiar entre versiones menores. No codificar una suposición a partir de una sola medición.
Rendimiento
Sección titulada «Rendimiento»El diseño de streaming acota la memoria máxima. El motor entregado vuelca cada página completada y libera su búfer, de modo que el conjunto residente no crece con el número de páginas, a diferencia del escritor en memoria. El motor descarga la contabilidad de referencias cruzadas y del árbol de páginas en flujos temporales respaldados por disco para mantener casi constante la huella del proceso. Las cifras concretas de memoria y de tiempo total son una propiedad del nivel experimental y pueden cambiar entre versiones menores; por eso, aquí no se afirma ningún número fijo. El performance_budget de 1500 ms de tiempo total y 64 MB de pico es el margen del lienzo, no una garantía contractual. La reproducibilidad es bitwise: el mismo contenido y la misma configuración producen una salida idéntica byte a byte, fijada por las pruebas de línea base del motor.
Notas de seguridad
Sección titulada «Notas de seguridad»El writeContent() del cursor es una vía de escape de bajo nivel. Añade los bytes proporcionados al flujo de contenido de la página tal cual y no valida la sintaxis ni la semántica de los operadores. Pasar entrada no confiable a writeContent() produce un PDF corrupto o malicioso. El llamador debe tratar ese método como una superficie reservada solo para entrada confiable y preferir writeText() para cualquier texto influido por el llamador. El cursor entregado escapa el texto pasado a writeText() según la gramática de cadenas literales de PDF, pero no sanea los operadores en crudo. El modelo en el que el flujo es propiedad del llamador también es una propiedad de seguridad. El motor escribe en el flujo, pero nunca lo cierra ni lo reabre, de modo que no puede redirigir la salida. La superficie de ataque en tiempo de ejecución es real porque el motor se entrega. La obligación de los llamadores es no alimentar nunca bytes no confiables a writeContent(), y la del motor, respetar las invariantes del contrato.
Conformidad
Sección titulada «Conformidad»| Afirmación | Estándar | Cláusula | Evidencia |
|---|---|---|---|
| Un flujo de contenido codifica el contenido de la página como una secuencia de operadores gráficos, que el cursor añade. | ISO 32000-2 | §8 | |
| El escritor emite, al cerrar, una estructura de referencias cruzadas que asigna a cada número de objeto su desplazamiento en bytes. | ISO 32000-2 | §7 |
Ambas cláusulas están fijadas en el glosario y se presentan parafraseadas. NextPDF no reproduce ningún texto normativo. El ADR del escritor de streaming al que hace referencia el PHPDoc del contrato contiene la justificación del ciclo de vida y del alcance.
Contexto comercial
Sección titulada «Contexto comercial»Core de código abierto incluye un motor de streaming probado detrás de estos contratos experimental. Las clases del motor son internas; por tanto, el streaming se consume a través del contrato público, no mediante un nombre de clase concreto. NextPDF Pro y NextPDF Enterprise siguen el mismo contrato, así que el código escrito contra StreamingWriterInterface en Core sigue siendo válido frente a una implementación Premium del mismo contrato. La advertencia es el nivel experimental, no la edición ni la disponibilidad. La firma puede cambiar en una versión menor con aviso previo de obsolescencia.
Véase también
Sección titulada «Véase también»- Contratos: 41 interfaces públicas (SPI) — la descripción general de la SPI y los niveles de estabilidad.
- Contratos / Document — el escritor en memoria que estos contratos complementan.
- Writer — el emisor de objetos PDF y de referencias cruzadas.
- HTML / Restricciones de streaming (ADR-001) — la justificación del alcance del streaming.
- Rendimiento — la justificación de la salida en streaming ligada a la memoria.