跳到內容

從控制器(controller)回傳已產生的 PDF

在控制器(controller)action 內產生 PDF,並透過 HTTP 回應回傳。 每個框架整合都提供一個 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。 繼續之前,請在你框架的安裝頁面確認探索結果。

每個框架整合都共用同樣的三段式結構:取得全新文件的方式、在該文件上輸出內容的一組呼叫,以及一個可將完成文件轉成 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() 取得一份未經包裝的文件。
關注點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 建構期間阻塞該請求。 對於單頁文件,通常仍落在一般請求的時間預算之內。 對於多頁或批次輸出,請用佇列工作把產生工作移出請求執行緒——參見 在佇列工作中產生 PDF。 串流版本能降低大型文件的尖峰記憶體,代價是 Content-Length 未知。 當限制條件是記憶體,且不需要進度條時,就選用它們。

  • 這些 PdfResponse 工廠會套用一組固定的回應強化標頭,並在每個整合中清理下載檔名。 不要再自行加入那些標頭。
  • 絕不要把未經驗證的使用者輸入直接插進你傳給工廠的檔名中。 請傳入一個你能掌控的值,並讓工廠把清理當作第二層防護。
  • 在 catch 區塊中,請記錄例外類別與一個關聯識別碼,而非例外訊息或追蹤。 日誌接收端中的原始追蹤會造成資訊洩漏。
  • 絕不要寫一個空的 catch 區塊。 這裡每個範例都會記錄日誌並回傳一個已定義的錯誤回應。

每個整合的「安全與維運」頁面都記錄了該整合的威脅模型:標頭集合、檔名清理規則,以及文件繫結的生命週期。

本指南未提出任何規範性標準主張。 所示的每個 API 呼叫都是指定整合的已驗證公開介面,並已對照各套件的快速入門與正式環境用法頁面交叉核對。 「另請參閱」底下連結的上游正式環境用法頁面,記錄了這些整合所依賴的標頭語意與容器繫結行為,以及其 PSR 引用。 本 cookbook 頁面重述其用法,並把規範性引用留給那些頁面。