從控制器(controller)回傳已產生的 PDF
在控制器(controller)action 內產生 PDF,並透過 HTTP 回應回傳。 每個框架整合都提供一個 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。 繼續之前,請在你框架的安裝頁面確認探索結果。
概念總覽
標題為「概念總覽」的區段每個框架整合都共用同樣的三段式結構:取得全新文件的方式、在該文件上輸出內容的一組呼叫,以及一個可將完成文件轉成 HTTP 回應的 PdfResponse 工廠(factory)。 文件 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——可透過
Services::pdf()(或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 建構期間阻塞該請求。 對於單頁文件,通常仍落在一般請求的時間預算之內。 對於多頁或批次輸出,請用佇列工作把產生工作移出請求執行緒——參見 在佇列工作中產生 PDF。 串流版本能降低大型文件的尖峰記憶體,代價是 Content-Length 未知。 當限制條件是記憶體,且不需要進度條時,就選用它們。
安全注意事項
標題為「安全注意事項」的區段- 這些
PdfResponse工廠會套用一組固定的回應強化標頭,並在每個整合中清理下載檔名。 不要再自行加入那些標頭。 - 絕不要把未經驗證的使用者輸入直接插進你傳給工廠的檔名中。 請傳入一個你能掌控的值,並讓工廠把清理當作第二層防護。
- 在 catch 區塊中,請記錄例外類別與一個關聯識別碼,而非例外訊息或追蹤。 日誌接收端中的原始追蹤會造成資訊洩漏。
- 絕不要寫一個空的
catch區塊。 這裡每個範例都會記錄日誌並回傳一個已定義的錯誤回應。
每個整合的「安全與維運」頁面都記錄了該整合的威脅模型:標頭集合、檔名清理規則,以及文件繫結的生命週期。
符合性
標題為「符合性」的區段本指南未提出任何規範性標準主張。 所示的每個 API 呼叫都是指定整合的已驗證公開介面,並已對照各套件的快速入門與正式環境用法頁面交叉核對。 「另請參閱」底下連結的上游正式環境用法頁面,記錄了這些整合所依賴的標頭語意與容器繫結行為,以及其 PSR 引用。 本 cookbook 頁面重述其用法,並把規範性引用留給那些頁面。
另請參閱
標題為「另請參閱」的區段- 在佇列工作中產生 PDF——把這項工作移出請求執行緒。
- Laravel 正式環境用法——已接好 DI 的控制器、標頭集合,以及 PSR-11 繫結合約。
- Symfony 快速入門——控制器、inline、串流,以及回應模型。
- CodeIgniter 快速入門——
Services::pdf()、pdf()輔助函式,以及PdfResponse。 - 選擇一個整合——選擇合適的框架套件。