Ir al contenido

NextPDF Symfony en producción

El bundle está diseñado para runtimes de PHP de larga duración. Los documentos no se comparten, el registro de fuentes queda bloqueado tras el precalentamiento y la caché de imágenes se reinicia entre solicitudes. Usar streaming para los PDF de gran tamaño y delegar los trabajos pesados en los workers de Messenger.

Ciclo de vida seguro de los servicios para workers

Sección titulada «Ciclo de vida seguro de los servicios para workers»

Los runtimes de larga duración mantienen vivo el contenedor entre solicitudes, por lo que el estado por solicitud no debe filtrarse. FrankenPHP, RoadRunner y los workers de Messenger funcionan de esta manera. El services.php del bundle codifica el ciclo de vida descrito a continuación, verificado contra las definiciones de servicio:

  • Document: no compartido. nextpdf.document (y los alias PdfDocumentInterface / Document) se resuelve como una instancia nueva cada vez. Según PSR-11, un contenedor puede devolver legítimamente un valor distinto en cada get() para el mismo id (PSR-11 §1.1.2). Resolver un documento por solicitud. No conservarlo nunca entre solicitudes.
  • FontRegistry: compartido y bloqueado. El registro es un singleton que vive durante todo el proceso. Tras warmup() (cuando preload_fonts no está vacío), el compiler pass llama a lock(). El bloqueo impide mutaciones en tiempo de ejecución y, por tanto, la contaminación del estado de las fuentes entre solicitudes.
  • ImageRegistry: compartido, reiniciado por solicitud. La caché LRU de imágenes se comparte, pero está etiquetada con kernel.reset con el método reset, de modo que Symfony la limpia entre solicitudes en los runtimes que respetan kernel.reset.
  • Contratos de EInvoice: no compartidos. Cuando hay implementaciones de Premium presentes, los servicios de embedder, validador, perfil y schematron se registran como no compartidos. El contexto del parser de cada llamada nunca se filtra entre solicitudes.

Inyectar PdfFactory —un contenedor de configuración compartido y sin estado— y llamar a create() por solicitud:

public function __construct(private readonly PdfFactory $pdf) {}
public function action(): Response
{
$doc = $this->pdf->create(); // fresh, disposable
// ... build ...
return PdfResponse::inline($doc, 'document.pdf');
}

No inyectar Document ni nextpdf.document en un servicio que sea compartido y se conserve entre solicitudes. En su lugar, resolverlo dentro del método con alcance de solicitud.

PdfResponse::streamDownload() y streamInline() devuelven un StreamedResponse. Su callback emite el cuerpo del PDF en bloques de 64 KB y vacía el búfer después de cada bloque. Esto acota el búfer de respuesta para documentos de gran tamaño. Las dos salvedades descritas a continuación están verificadas contra PdfResponse:

  • Las variantes con streaming omiten Content-Length de forma intencionada (el objeto de respuesta no conoce de antemano el tamaño del cuerpo). Las barras de progreso de descarga y algunos proxies prefieren una longitud conocida. Usar los métodos sin streaming download() o inline() cuando el documento sea lo bastante pequeño como para mantenerlo en memoria y convenga una longitud de contenido.
  • Las variantes con streaming emiten las mismas cabeceras de seguridad y el mismo Cache-Control: private, max-age=0, must-revalidate que las variantes con búfer.

Elegir streaming para informes de varios megabytes y exportaciones por lotes. Elegir las variantes con búfer para respuestas pequeñas y sensibles a la latencia.

Delegar la generación en Messenger cuando las solicitudes deban responder rápido o cuando el renderizado consuma mucha CPU.

  1. Implementar PdfBuilderInterface para cada tipo de documento.
  2. Registrar los builders en un container.service_locator y conectarlo en GeneratePdfHandler como su $builderLocator.
  3. Enrutar GeneratePdfMessage a un transporte duradero.
  4. Ejecutar los workers con tiempos de vida acotados.

Reciclar los workers para que una fuga de memoria en una dependencia de terceros no pueda crecer sin límite:

Ventana de terminal
php bin/console messenger:consume async \
--limit=200 \
--memory-limit=256M \
--time-limit=3600

Las claves de configuración messenger.timeout y messenger.retries del bundle registran el tiempo de espera por mensaje y el presupuesto de reintentos previstos. Aplicar el comportamiento correspondiente mediante la estrategia de reintentos de Symfony y los flags del worker.

Seguridad de la ruta de salida en los workers

Sección titulada «Seguridad de la ruta de salida en los workers»

GeneratePdfMessage valida la ruta de salida al construirse. Después, GeneratePdfHandler la revalida durante la ejecución, antes de escribir en disco. Esta comprobación en dos etapas es importante para el trabajo asíncrono. Un mensaje puede permanecer en una cola entre el envío y el consumo, por lo que el handler no confía a ciegas en la ruta encolada. Restringir los permisos del sistema de archivos del worker al directorio de salida previsto como defensa en profundidad.

Los servicios FontRegistry e ImageRegistry aceptan un Psr\Log\LoggerInterface opcional (vinculado mediante nullOnInvalid()). Cuando la aplicación proporciona un logger, esos registros pueden emitir diagnósticos a través de él. El logger es un colaborador opcional e intercambiable según el contrato de logger de PSR-3 (PSR-3). Para obtener visibilidad a nivel de solicitud, añadir logging alrededor de PdfFactory::create() y del handler de Messenger en el código de la aplicación. Usar messenger:consume -vv durante el triaje de incidentes.

  • Fijar un único major de nextpdf/core en el composer.json de la aplicación (el bundle acepta ^3.0 || ^5.2).
  • Asegurarse de que ext-mbstring y ext-zlib estén habilitadas en la imagen de PHP desplegada (de lo contrario, el bundle falla de inmediato al arrancar).
  • Rellenar de antemano preload_fonts con las fuentes que usan los documentos para que el registro se caliente y se bloquee al arrancar en lugar de en la primera solicitud.
  • Apuntar cache_path a una ubicación persistente y con permisos de escritura si se depende de artefactos en caché entre despliegues. De lo contrario, el valor predeterminado %kernel.cache_dir% es suficiente.
  • Ejecutar php bin/console cache:warmup durante el despliegue para que el contenedor compilado (incluidas las sondas de extensiones opcionales) se construya antes de que llegue el tráfico.
  • Usar un transporte de Messenger duradero (no sync) para el trabajo asíncrono en producción, y reciclar los workers con --limit / --memory-limit / --time-limit.
  • Respuestas con streaming detrás de un proxy con búfer: un proxy que almacena en búfer el cuerpo completo anula el beneficio de memoria. Configurar el proxy para que transmita por streaming las respuestas PDF, o usar respuestas con búfer en ese caso.
  • kernel.reset no respetado: en un runtime que no llama a kernel.reset, la caché de imágenes está acotada por image_cache_mb pero no se limpia entre solicitudes; dimensionar el límite en consecuencia.
  • Retener un documento entre solicitudes: un Document capturado de una solicitud anterior conservará estado obsoleto. Resolver siempre por solicitud mediante PdfFactory.

Cada fila contiene una afirmación normativa hecha en esta página, anclada a un reference_id completo de 64 dígitos hexadecimales del corpus SDO con publish gate. La procedencia, es decir, el manifiesto del corpus y el transporte de recuperación, está en _sidecars/rag-citations.yaml.

EspecificaciónCláusulareference_idAfirmación
PSR-11psr_11_container#1.1.2.p3.bServicio no compartido: valor distinto por resolución
PSR-3psr_3_logger#x3.p17Colaborador logger opcional
  • /integrations/symfony/configuration/: ciclo de vida de los servicios y parámetros.
  • /integrations/symfony/security-and-operations/: cabeceras de respuesta, validación de rutas, gestión de claves.
  • /integrations/symfony/troubleshooting/: diagnósticos de arranque y en tiempo de ejecución.
  • /integrations/symfony/quickstart/: configuración asíncrona mínima.