跳转到内容

从控制器(controller)返回生成的 PDF

在控制器(controller)action 中生成 PDF,并将其作为 HTTP 响应返回。每个 Framework(框架)集成都提供一个 PdfResponse 辅助工具,它会为该框架构建响应对象、设置 Content-Type: application/pdf、附加安全标头并清理文件名。本指南涵盖三种传递模式——浏览器内预览、文件下载与流式传递——适用于 Laravel、Symfony 与 CodeIgniter 4。

先确认以下前提,避免任务执行到一半时被卡住:

  • 已安装 NextPDF 核心。
  • 已安装其中一个框架集成,且其 service provider、bundle 或 service 已被发现。开始前,请在你所用框架的安装页面确认发现结果。
  • 流式模式不需要额外的包。每个集成都会在缓冲版本之外,同时提供流式版本。

这是一篇 how-to,假设你已经知道如何在自己的框架中把请求路由到控制器。若想查看各框架第一个可执行示例,请阅读“另请参阅”下方链接的框架快速入门。

安装与你所用框架相符的集成。执行以下任意一行。

Terminal window
composer require nextpdf/laravel
Terminal window
composer require nextpdf/symfony
Terminal window
composer require nextpdf/codeigniter

如果使用 Laravel,安装后请发布配置文件。

Terminal window
php artisan vendor:publish --tag=nextpdf-config

Symfony 会通过 Flex 自动注册 bundle,CodeIgniter 会自动发现 service。继续之前,请在你所用框架的安装页面确认发现结果。

每个框架集成都共用同样的三段式结构:一种获取全新文件的方式、一组在该文件上输出内容的调用,以及一个 PdfResponse 工厂(factory),用于把完成的文件转换为 HTTP 响应。文件 API(addPage()cell()setFont())是核心引擎接口,在各框架间完全相同。响应工厂的差异只体现在返回的响应类上,因为每个框架都有自己的 HTTP 响应类型。

PdfResponse 提供三种传递模式。Inline 会设置 Content-Disposition: inline 标头,让浏览器在查看器标签页中渲染 PDF。Download 会设置 Content-Disposition: attachment,让浏览器把文件保存下来。Streamed 会以固定块输出 PDF 主体,而不是把整份文件缓冲到内存中。当峰值内存比已知的 Content-Length 更重要时,就选用它。

通过各框架惯用的 resolve(解析)路径获取文件:

  • Laravel——通过 app(...) 从容器(container)解析 NextPDF\Contracts\DocumentFactoryInterface,并调用 create();它会返回一份全新的 NextPDF\Core\Document——也就是 PdfResponse 工厂所接受的具体类型。
  • Symfony——注入 NextPDF\Symfony\Service\PdfFactory 并调用 create();它会返回一份全新的 NextPDF\Core\Document,并已应用配置好的文件默认值。
  • CodeIgniter 4——解析 Pdf 库时,可以通过 Services::pdf()(或 pdf() 辅助函数);也可以通过 pdf_document() 获取一份未经包装的文件。
关注点LaravelSymfonyCodeIgniter 4
全新文件app(DocumentFactoryInterface::class)->create()PdfFactory::create()pdf_document() / Services::pdf()->document()
Inline 响应PdfResponse::inline($doc, $name)PdfResponse::inline($doc, $name)$pdf->inline($name) / PdfResponse::inline($doc, $name)
Download 响应PdfResponse::download($doc, $name)PdfResponse::download($doc, $name)$pdf->download($name) / PdfResponse::download($doc, $name)
流式 inlinePdfResponse::streamInline($doc, $name)PdfResponse::streamInline($doc, $name)PdfResponse::streamInline($doc, $name)
流式 downloadPdfResponse::streamDownload($doc, $name)PdfResponse::streamDownload($doc, $name)PdfResponse::streamDownload($doc, $name)
返回类型Illuminate\Http\Response(流式时:StreamedResponseSymfony\Component\HttpFoundation\Response(流式时:StreamedResponseCodeIgniter\HTTP\DownloadResponse

Laravel 的 PdfResponse 位于 NextPDF\Laravel\Http\PdfResponse,Symfony 的位于 NextPDF\Symfony\Http\PdfResponse,CodeIgniter 的位于 NextPDF\CodeIgniter\Http\PdfResponse。每个集成的“安全与运维”页面都记录了该包完整的响应行为——标头集合、disposition 规则与文件名清理。那些页面都链接在“另请参阅”下方。

以下是每个框架中最精简的 download action。文件调用使用同一套核心接口,只有控制器骨架代码不同。

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

若想在浏览器中预览而不是下载,在 Laravel 与 Symfony 中将 download(...) 调用换成 inline(...),在 CodeIgniter 中则换成 $pdf->inline('report.pdf')。disposition 会变成 inline,其余所有标头都保持不变。

生产环境的 action 会注入其依赖项、捕获该集成所记录的最具体异常、在不泄漏堆栈跟踪的前提下记录失败的类,并返回一个已定义的 HTTP 错误。下面的示例使用 Laravel 的构造函数注入。Symfony 与 CodeIgniter 的对应写法遵循同样的结构,并记录在各集成的“生产环境用法”页面上。

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

在每个 action 中注入 DocumentFactoryInterface 并调用 create()。这会返回一份全新的 NextPDF\Core\Document——也就是 Laravel 的 PdfResponse 工厂所接受的具体类型。每次请求都解析一份全新文件,可让工厂在测试中保持可替换。不要在单个长时间运行的 worker 程序中,用同一个控制器实例处理两份不相关的文件。

对于非常大的文件,请把缓冲工厂换成流式工厂,以限制峰值内存。流式版本会返回一个 StreamedResponse(Laravel 与 Symfony),并以固定块输出响应体。它会刻意省略 Content-Length,因此下载进度条与对长度敏感的 proxy 都看不到已知大小。对于体量较小、对延迟敏感的响应,请优先使用缓冲的 download() / inline()

Laravel: streamed download for a large report
$document = $this->documents->create();
// ... emit content onto $document ...
return PdfResponse::streamDownload($document, 'annual-report.pdf');
  • 每次调用都用全新文件。 在三个集成中,文件都由工厂创建,每次解析都会得到全新实例。不要跨多份逻辑文件缓存同一份已解析的文件,也不要在长时间运行的 worker 中跨多次请求缓存它。陈旧的内容状态会残留下来。
  • 空白文件名。 传给 PdfResponse 工厂的空白文件名会回退到一个默认名称(document.pdf),而不会产生空白的 disposition。请传入明确且有意义的文件名。
  • 非 ASCII 文件名。 Laravel 响应会为非 ASCII 名称自动加上 RFC 5987 的 filename*= 参数,而 ASCII 名称则使用常规参数。不要自己手动编码文件名。
  • 位于缓冲 proxy 后面的流式响应。 会缓冲整个响应体的 proxy 会抵消流式带来的内存收益。请把 proxy 配置为流式传递 PDF 响应,或在该路径上改用缓冲响应。
  • Symfony 流式回调。 Symfony 的流式版本会返回一个 StreamedResponse,其回调会将输出 flush 出去。在把响应交还之后,不要自己再写入响应体。

在控制器内同步生成 PDF,会在整份 PDF 构建期间阻塞该请求。对于单页文件,这通常仍在常规请求的时间预算之内。对于多页或批量输出,请使用队列 worker 把生成工作移出请求线程——参见 在队列 worker 中生成 PDF。流式版本能降低大型文件的峰值内存,代价是 Content-Length 未知。当内存是限制条件且不需要进度条时,就选用它们。

  • 这些 PdfResponse 工厂会应用一组固定的响应加固标头,并在每个集成中清理下载文件名。不要再自行添加这些标头。
  • 绝不要把未经验证的用户输入直接插进你传给工厂的文件名中。请传入你可控的值,并把工厂清理视为第二层防护。
  • 在 catch 块中,请记录异常类与一个关联标识符,而不是异常消息或跟踪。日志接收端中的原始跟踪属于信息泄漏。
  • 绝不要写一个空的 catch 块。本文每个示例都会记录日志并返回一个已定义的错误响应。

每个集成的“安全与运维”页面都记录了该集成的威胁模型——标头集合、文件名清理规则,以及文件绑定的生命周期。

本指南未提出任何规范性标准声明。所示的每个 API 调用都是对应集成已验证的公开接口,并已对照各包的快速入门与生产环境用法页面交叉核对。“另请参阅”下方链接的上游生产环境用法页面,记录了这些集成所依赖的标头语义与容器绑定行为,以及相关 PSR 引用。本 cookbook 页面重述其用法,并把规范性引用留给那些页面。