从控制器(controller)返回生成的 PDF
在控制器(controller)action 中生成 PDF,并将其作为 HTTP 响应返回。每个 Framework(框架)集成都提供一个 PdfResponse 辅助工具,它会为该框架构建响应对象、设置 Content-Type: application/pdf、附加安全标头并清理文件名。本指南涵盖三种传递模式——浏览器内预览、文件下载与流式传递——适用于 Laravel、Symfony 与 CodeIgniter 4。
先确认以下前提,避免任务执行到一半时被卡住:
- 已安装 NextPDF 核心。
- 已安装其中一个框架集成,且其 service provider、bundle 或 service 已被发现。开始前,请在你所用框架的安装页面确认发现结果。
- 流式模式不需要额外的包。每个集成都会在缓冲版本之外,同时提供流式版本。
这是一篇 how-to,假设你已经知道如何在自己的框架中把请求路由到控制器。若想查看各框架第一个可执行示例,请阅读“另请参阅”下方链接的框架快速入门。
安装与你所用框架相符的集成。执行以下任意一行。
composer require nextpdf/laravelcomposer require nextpdf/symfonycomposer require nextpdf/codeigniter如果使用 Laravel,安装后请发布配置文件。
php artisan vendor:publish --tag=nextpdf-configSymfony 会通过 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()获取一份未经包装的文件。
API 接口
标题为“API 接口”的章节| 关注点 | Laravel | Symfony | CodeIgniter 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) |
| 流式 inline | PdfResponse::streamInline($doc, $name) | PdfResponse::streamInline($doc, $name) | PdfResponse::streamInline($doc, $name) |
| 流式 download | PdfResponse::streamDownload($doc, $name) | PdfResponse::streamDownload($doc, $name) | PdfResponse::streamDownload($doc, $name) |
| 返回类型 | Illuminate\Http\Response(流式时:StreamedResponse) | Symfony\Component\HttpFoundation\Response(流式时:StreamedResponse) | CodeIgniter\HTTP\DownloadResponse |
Laravel 的 PdfResponse 位于 NextPDF\Laravel\Http\PdfResponse,Symfony 的位于 NextPDF\Symfony\Http\PdfResponse,CodeIgniter 的位于 NextPDF\CodeIgniter\Http\PdfResponse。每个集成的“安全与运维”页面都记录了该包完整的响应行为——标头集合、disposition 规则与文件名清理。那些页面都链接在“另请参阅”下方。
代码示例——快速开始
标题为“代码示例——快速开始”的章节以下是每个框架中最精简的 download action。文件调用使用同一套核心接口,只有控制器骨架代码不同。
<?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'); }}<?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'); }}<?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 的对应写法遵循同样的结构,并记录在各集成的“生产环境用法”页面上。
<?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()。
$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 页面重述其用法,并把规范性引用留给那些页面。
另请参阅
标题为“另请参阅”的章节- 在队列 worker 中生成 PDF——把这项工作移出请求线程。
- Laravel 生产环境用法——已接入 DI 的控制器、标头集合,以及 PSR-11 绑定合约。
- Symfony 快速入门——控制器、inline、流式传递以及响应模型。
- CodeIgniter 快速入门——
Services::pdf()、pdf()辅助函数,以及PdfResponse。 - 选择一个集成——选择合适的框架包。