Ir al contenido

Devolver un PDF generado desde un controlador

Genera un PDF en la acción de un controlador y devuélvelo como respuesta HTTP. Cada integración de framework incluye un ayudante PdfResponse que construye el objeto de respuesta del framework correspondiente, establece Content-Type: application/pdf, adjunta las cabeceras de seguridad y sanea el nombre de archivo. Esta guía cubre los tres modos de entrega —vista previa en línea, descarga de archivo y entrega por streaming— para Laravel, Symfony y CodeIgniter 4.

Revisa primero estos requisitos previos para evitar interrupciones a mitad de la tarea:

  • El core de NextPDF está instalado.
  • Hay una integración de framework instalada y se ha descubierto su service provider, bundle o servicio. Verifica el descubrimiento en la página de instalación de tu framework antes de empezar.
  • El modo por streaming no requiere paquetes adicionales. Cada integración incluye la variante por streaming junto con la variante con búfer.

Este es un tutorial práctico. Asume que ya sabes cómo enrutar una solicitud hacia un controlador en tu framework. Para ver el primer ejemplo ejecutable de cada framework, consulta el quickstart del framework enlazado en Véase también.

Instala la integración que corresponda a tu framework. Ejecuta uno de los siguientes comandos:

Ventana de terminal
composer require nextpdf/laravel
Ventana de terminal
composer require nextpdf/symfony
Ventana de terminal
composer require nextpdf/codeigniter

Para Laravel, publica la configuración después de la instalación.

Ventana de terminal
php artisan vendor:publish --tag=nextpdf-config

Symfony registra automáticamente el bundle a través de Flex y CodeIgniter descubre automáticamente el servicio. Confirma el descubrimiento en la página de instalación de tu framework antes de continuar.

Cada integración de framework comparte la misma estructura de tres partes: una forma de obtener un documento nuevo, un conjunto de llamadas para escribir contenido en ese documento y una fábrica PdfResponse que convierte el documento terminado en una respuesta HTTP. La API del documento (addPage(), cell(), setFont()) es la superficie del motor core y es idéntica en todos los frameworks. La fábrica de respuestas solo difiere en la clase de respuesta que devuelve, porque cada framework tiene su propio tipo de respuesta HTTP.

PdfResponse ofrece tres modos de entrega. En línea establece una cabecera Content-Disposition: inline para que el navegador muestre el PDF en una pestaña del visor. Descarga establece Content-Disposition: attachment para que el navegador guarde el archivo. Por streaming emite el cuerpo del PDF en fragmentos de tamaño fijo, en lugar de almacenar en búfer todo el documento en memoria. Elígelo para documentos grandes donde el pico de memoria importa más que un Content-Length conocido.

Obtén el documento mediante la ruta de resolución idiomática del framework:

  • Laravel — resuelve NextPDF\Contracts\DocumentFactoryInterface desde el contenedor con app(...) y llama a create(), que devuelve un NextPDF\Core\Document nuevo —el tipo concreto que aceptan las fábricas PdfResponse.
  • Symfony — inyecta NextPDF\Symfony\Service\PdfFactory y llama a create(), que devuelve un NextPDF\Core\Document nuevo con los valores predeterminados del documento ya aplicados.
  • CodeIgniter 4 — resuelve la biblioteca Pdf mediante Services::pdf() (o el ayudante pdf()), u obtén un documento básico mediante pdf_document().
AspectoLaravelSymfonyCodeIgniter 4
Documento nuevoapp(DocumentFactoryInterface::class)->create()PdfFactory::create()pdf_document() / Services::pdf()->document()
Respuesta en líneaPdfResponse::inline($doc, $name)PdfResponse::inline($doc, $name)$pdf->inline($name) / PdfResponse::inline($doc, $name)
Respuesta de descargaPdfResponse::download($doc, $name)PdfResponse::download($doc, $name)$pdf->download($name) / PdfResponse::download($doc, $name)
En línea por streamingPdfResponse::streamInline($doc, $name)PdfResponse::streamInline($doc, $name)PdfResponse::streamInline($doc, $name)
Descarga por streamingPdfResponse::streamDownload($doc, $name)PdfResponse::streamDownload($doc, $name)PdfResponse::streamDownload($doc, $name)
Tipo devueltoIlluminate\Http\Response (por streaming: StreamedResponse)Symfony\Component\HttpFoundation\Response (por streaming: StreamedResponse)CodeIgniter\HTTP\DownloadResponse

El PdfResponse de Laravel reside en NextPDF\Laravel\Http\PdfResponse, el de Symfony en NextPDF\Symfony\Http\PdfResponse, y el de CodeIgniter en NextPDF\CodeIgniter\Http\PdfResponse. La página de seguridad y operaciones de cada integración documenta el comportamiento de respuesta completo de cada paquete: conjunto de cabeceras, reglas de disposición y saneamiento del nombre de archivo. Esas páginas están enlazadas en la sección Véase también.

Aquí tienes la acción mínima de descarga en cada framework. Las llamadas al documento usan la misma superficie core. Solo difiere el andamiaje del controlador.

Laravel: app/Http/Controllers/ReportController.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Response;
use NextPDF\Contracts\DocumentFactoryInterface;
use NextPDF\Laravel\Http\PdfResponse;
final class ReportController extends Controller
{
public function download(): Response
{
$document = app(DocumentFactoryInterface::class)->create();
$document->addPage();
$document->cell(0, 10, 'Monthly report', newLine: true);
return PdfResponse::download($document, 'report.pdf');
}
}
Symfony: src/Controller/ReportController.php
<?php
declare(strict_types=1);
namespace App\Controller;
use NextPDF\Symfony\Http\PdfResponse;
use NextPDF\Symfony\Service\PdfFactory;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class ReportController
{
#[Route('/report', name: 'report_pdf')]
public function download(PdfFactory $pdf): Response
{
$document = $pdf->create();
$document->addPage();
$document->cell(0, 10, 'Monthly report', newLine: true);
return PdfResponse::download($document, 'report.pdf');
}
}
CodeIgniter 4: app/Controllers/ReportController.php
<?php
declare(strict_types=1);
namespace App\Controllers;
use CodeIgniter\HTTP\DownloadResponse;
use NextPDF\CodeIgniter\Config\Services;
final class ReportController extends BaseController
{
public function download(): DownloadResponse
{
$pdf = Services::pdf();
$pdf->document()->addPage();
$pdf->document()->cell(0, 10, 'Monthly report');
return $pdf->download('report.pdf');
}
}

Para mostrar una vista previa en el navegador en lugar de descargar, cambia la llamada a download(...) por inline(...) en Laravel y Symfony, o por $pdf->inline('report.pdf') en CodeIgniter. La disposición pasa a inline, y todas las demás cabeceras permanecen iguales.

Una acción de producción inyecta sus dependencias, captura la excepción más específica que la integración documenta, registra la clase del fallo sin exponer ninguna traza y devuelve un error HTTP definido. El ejemplo siguiente usa la inyección por constructor de Laravel. Los equivalentes de Symfony y CodeIgniter siguen la misma estructura y están documentados en la página de uso en producción de cada integración.

Laravel: app/Http/Controllers/InvoiceController.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Response;
use NextPDF\Contracts\DocumentFactoryInterface;
use NextPDF\Laravel\Http\PdfResponse;
use Psr\Log\LoggerInterface;
use Throwable;
final class InvoiceController extends Controller
{
public function __construct(
private readonly DocumentFactoryInterface $documents,
private readonly LoggerInterface $logger,
) {}
public function show(int $invoiceId): Response
{
try {
$document = $this->documents->create();
$document->addPage();
$document->cell(0, 10, "Invoice #{$invoiceId}", newLine: true);
return PdfResponse::download(
$document,
"invoice-{$invoiceId}.pdf",
);
} catch (Throwable $exception) {
// Log the exception class, never the message or a stack trace,
// so internal detail does not leak into the log sink.
$this->logger->error('Invoice PDF generation failed', [
'invoice_id' => $invoiceId,
'exception' => $exception::class,
]);
return new Response('Could not generate the invoice PDF.', 500);
}
}
}

Inyecta DocumentFactoryInterface y llama a create() en cada acción. Esto devuelve un NextPDF\Core\Document nuevo —el tipo concreto que aceptan las fábricas PdfResponse de Laravel. Resolver un documento nuevo por solicitud conserva la posibilidad de sustituir la fábrica en las pruebas. No reutilices una misma instancia de controlador para dos documentos no relacionados dentro de un único proceso worker de larga duración.

Para documentos muy grandes, reemplaza la fábrica con búfer por una variante por streaming para acotar el pico de memoria. La variante por streaming devuelve un StreamedResponse (Laravel y Symfony) y emite el cuerpo en fragmentos de tamaño fijo. Omite deliberadamente Content-Length, por lo que las barras de progreso de descarga y los proxies sensibles a la longitud no ven un tamaño conocido. Prefiere los métodos con búfer download() / inline() para respuestas pequeñas y sensibles a la latencia.

Laravel: streamed download for a large report
$document = $this->documents->create();
// ... emit content onto $document ...
return PdfResponse::streamDownload($document, 'annual-report.pdf');
  • Un documento nuevo por llamada. En las tres integraciones, el documento lo crea una fábrica y es nuevo en cada resolución. No almacenes en caché un documento resuelto entre documentos lógicos, ni entre solicitudes en un worker de larga duración. El estado de contenido obsoleto se arrastra entre usos.
  • Nombre de archivo vacío. Un nombre de archivo vacío pasado a una fábrica PdfResponse usa un nombre predeterminado (document.pdf) en lugar de producir una disposición en blanco. Pasa un nombre de archivo explícito y significativo.
  • Nombres de archivo no ASCII. La respuesta de Laravel agrega automáticamente un parámetro RFC 5987 filename*= para los nombres no ASCII, y los nombres ASCII usan el parámetro simple. No codifiques manualmente el nombre de archivo.
  • Respuestas por streaming detrás de un proxy con búfer. Un proxy que almacena en búfer el cuerpo completo anula el beneficio de memoria del streaming. Configura el proxy para transmitir por streaming las respuestas PDF, o usa una respuesta con búfer en esa ruta.
  • Callback por streaming de Symfony. La variante por streaming de Symfony devuelve un StreamedResponse cuyo callback vacía la salida. No escribas por tu cuenta en el cuerpo de la respuesta después de devolverlo.

La generación síncrona dentro de un controlador bloquea la solicitud durante toda la construcción del PDF. Para un documento de una sola página, esto se mantiene cómodamente dentro de un presupuesto de solicitud típico. Para salidas de varias páginas o por lotes, mueve la generación fuera del hilo de la solicitud con un job en cola; consulta Generar un PDF en un job en cola. Las variantes por streaming reducen el pico de memoria en documentos grandes a costa de un Content-Length desconocido. Elígelas cuando la memoria sea la restricción y no se requiera una barra de progreso.

  • Las fábricas PdfResponse aplican un conjunto fijo de cabeceras de endurecimiento de respuesta y sanean el nombre de archivo de descarga en cada integración. No agregues esas cabeceras por tu cuenta.
  • Nunca interpoles entrada de usuario no validada directamente en un nombre de archivo que pases a la fábrica. Pasa un valor que controles y deja que la fábrica lo sanee como segunda capa.
  • En el bloque catch, registra la clase de la excepción y un identificador de correlación, no el mensaje ni la traza de la excepción. Una traza en bruto en un sink de logs constituye una fuga de información.
  • Nunca escribas un bloque catch vacío. Cada ejemplo de esta guía registra y devuelve una respuesta de error definida.

La página de seguridad y operaciones de cada integración documenta el modelo de amenazas de esa integración: conjunto de cabeceras, reglas de saneamiento del nombre de archivo y la vida útil del enlace del documento.

Esta guía no hace ninguna afirmación normativa sobre estándares. Cada llamada de API que se muestra pertenece a la superficie pública verificada de la integración nombrada, contrastada con las páginas de inicio rápido y de uso en producción de cada paquete. Las páginas upstream de uso en producción enlazadas en Véase también documentan la semántica de las cabeceras y el comportamiento de enlace al contenedor en el que se apoyan las integraciones, junto con sus citas PSR. Esta página del cookbook reformula el uso y delega las citas normativas a esas páginas.