Bỏ qua để đến nội dung

Trả về tệp PDF được tạo trong controller

Tạo tệp Portable Document Format (PDF) trong một controller action rồi trả về dưới dạng phản hồi Hypertext Transfer Protocol (HTTP). Mỗi tích hợp framework đều có helper PdfResponse để dựng đối tượng phản hồi, đặt Content-Type: application/pdf, gắn các header bảo mật và làm sạch tên tệp. Hướng dẫn này bao gồm ba chế độ phân phối cho Laravel, Symfony và CodeIgniter 4: xem trước inline, tải xuống và phân phối theo luồng.

Trước hết, hãy kiểm tra các điều kiện tiên quyết này để đường dẫn controller sẵn sàng trước khi bạn bắt đầu:

  • Lõi NextPDF đã được cài đặt.
  • Một tích hợp framework đã được cài đặt, đồng thời service provider, bundle hoặc service của nó đã được phát hiện. Hãy xác minh trạng thái phát hiện trên trang cài đặt cho framework của bạn trước khi bắt đầu.
  • Chế độ theo luồng không cần thêm gói nào. Mọi tích hợp đều có biến thể theo luồng bên cạnh biến thể được đệm.

Đây là hướng dẫn cách làm. Hướng dẫn giả định rằng bạn đã biết cách định tuyến một yêu cầu đến controller trong framework của mình. Để xem ví dụ đầu tiên có thể chạy trong từng framework, hãy đọc phần quickstart của framework được liên kết trong mục Xem thêm.

Cài đặt tích hợp cho framework của bạn. Hãy chạy một trong các lệnh sau.

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

Với Laravel, hãy xuất bản cấu hình sau khi cài đặt.

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

Symfony đăng ký bundle thông qua Flex, còn CodeIgniter tự động phát hiện service. Hãy xác nhận trạng thái phát hiện trên trang cài đặt của framework trước khi bạn tiếp tục.

Mọi tích hợp framework đều theo cùng một cấu trúc ba phần: bạn lấy một tài liệu mới, ghi nội dung vào tài liệu đó, rồi truyền tài liệu cho factory PdfResponse để trả về phản hồi HTTP. API tài liệu (addPage(), cell(), setFont()) là bề mặt cốt lõi của engine và giống nhau trên mọi framework. Factory phản hồi chỉ khác nhau ở lớp phản hồi mà nó trả về, vì mỗi framework có kiểu phản hồi HTTP riêng.

PdfResponse cung cấp ba chế độ phân phối. Inline đặt header Content-Disposition: inline để trình duyệt hiển thị PDF trong một tab xem tài liệu. Download đặt Content-Disposition: attachment để trình duyệt lưu tệp. Streamed phát phần thân PDF theo từng khối cố định thay vì đệm toàn bộ tài liệu trong bộ nhớ. Hãy chọn chế độ này cho các tài liệu lớn khi bộ nhớ đỉnh quan trọng hơn một Content-Length đã biết.

Hãy lấy tài liệu qua đường dẫn phân giải thông thường của framework:

  • Laravel — phân giải NextPDF\Contracts\DocumentFactoryInterface từ container bằng app(...) rồi gọi create(), hàm này trả về một NextPDF\Core\Document mới — kiểu cụ thể mà các factory PdfResponse chấp nhận.
  • Symfony — inject NextPDF\Symfony\Service\PdfFactory rồi gọi create(), hàm này trả về một NextPDF\Core\Document mới với các giá trị mặc định của tài liệu đã cấu hình được áp dụng sẵn.
  • CodeIgniter 4 — phân giải thư viện Pdf thông qua Services::pdf() (hoặc helper pdf()), hoặc lấy một tài liệu thuần thông qua pdf_document().
Nhu cầuLaravelSymfonyCodeIgniter 4
Tài liệu mớiapp(DocumentFactoryInterface::class)->create()PdfFactory::create()pdf_document() / Services::pdf()->document()
Phản hồi inlinePdfResponse::inline($doc, $name)PdfResponse::inline($doc, $name)$pdf->inline($name) / PdfResponse::inline($doc, $name)
Phản hồi tải xuốngPdfResponse::download($doc, $name)PdfResponse::download($doc, $name)$pdf->download($name) / PdfResponse::download($doc, $name)
Inline theo luồngPdfResponse::streamInline($doc, $name)PdfResponse::streamInline($doc, $name)PdfResponse::streamInline($doc, $name)
Tải xuống theo luồngPdfResponse::streamDownload($doc, $name)PdfResponse::streamDownload($doc, $name)PdfResponse::streamDownload($doc, $name)
Kiểu trả vềIlluminate\Http\Response (theo luồng: StreamedResponse)Symfony\Component\HttpFoundation\Response (theo luồng: StreamedResponse)CodeIgniter\HTTP\DownloadResponse

Lớp PdfResponse của Laravel nằm tại NextPDF\Laravel\Http\PdfResponse, của Symfony tại NextPDF\Symfony\Http\PdfResponse, và của CodeIgniter tại NextPDF\CodeIgniter\Http\PdfResponse. Trang security-and-operations của mỗi tích hợp mô tả đầy đủ hành vi phản hồi cho gói đó: tập header, quy tắc disposition và quy trình làm sạch tên tệp. Các trang đó được liên kết trong mục Xem thêm.

Dưới đây là download action tối thiểu trong mỗi framework. Các lệnh gọi tài liệu dùng cùng một bề mặt cốt lõi. Chỉ phần khung controller thay đổi.

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

Để xem trước trong trình duyệt thay vì tải xuống, hãy đổi lệnh gọi download(...) thành inline(...) trong Laravel và Symfony, hoặc $pdf->inline('report.pdf') trong CodeIgniter. Disposition sẽ đổi thành inline, còn mọi header khác giữ nguyên.

Một production action sẽ inject các phụ thuộc của nó, bắt ngoại lệ cụ thể nhất mà tích hợp ghi lại, ghi log lớp lỗi mà không làm lộ stack trace, và trả về một phản hồi lỗi HTTP đã xác định. Ví dụ bên dưới sử dụng constructor injection của Laravel. Các phiên bản tương đương của Symfony và CodeIgniter cùng theo mô thức này và xuất hiện trên trang production-usage của mỗi tích hợp.

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

Inject DocumentFactoryInterface và gọi create() trong mỗi action. Hàm này trả về một NextPDF\Core\Document mới — kiểu cụ thể mà các factory PdfResponse của Laravel chấp nhận. Việc phân giải tài liệu mới cho từng yêu cầu giúp bạn thay thế factory trong kiểm thử. Đừng tái sử dụng cùng một instance controller cho hai tài liệu không liên quan bên trong một tiến trình worker chạy lâu dài.

Với các tài liệu rất lớn, hãy thay factory được đệm bằng factory theo luồng để giới hạn bộ nhớ đỉnh. Biến thể theo luồng trả về một StreamedResponse (Laravel và Symfony) và phát phần thân theo từng khối cố định. Nó cố tình bỏ qua Content-Length, nên các thanh tiến trình tải xuống và các proxy nhạy với độ dài sẽ không thấy kích thước đã biết. Hãy ưu tiên download() / inline() được đệm cho các phản hồi nhỏ, nhạy với độ trễ.

Laravel: streamed download for a large report
$document = $this->documents->create();
// ... emit content onto $document ...
return PdfResponse::streamDownload($document, 'annual-report.pdf');

Trường hợp đặc biệt & điều cần lưu ý

Phần tiêu đề “Trường hợp đặc biệt & điều cần lưu ý”
  • Tài liệu mới cho mỗi lệnh gọi. Trong cả ba tích hợp, tài liệu là sản phẩm của factory và mới ở mỗi lần phân giải. Đừng cache một tài liệu đã phân giải cho nhiều tài liệu logic, hoặc qua nhiều yêu cầu trong một worker chạy lâu dài. Trạng thái nội dung cũ sẽ bị mang theo.
  • Tên tệp rỗng. Một tên tệp rỗng được truyền cho factory PdfResponse sẽ dùng tên mặc định (document.pdf) thay vì tạo ra một disposition trống. Hãy truyền một tên tệp tường minh, có ý nghĩa.
  • Tên tệp không phải ASCII. Phản hồi Laravel tự động thêm tham số RFC 5987 filename*= cho tên không phải ASCII, còn tên ASCII dùng tham số thông thường. Đừng tự mã hóa tên tệp bằng tay.
  • Phản hồi theo luồng đứng sau một proxy có đệm. Một proxy đệm toàn bộ phần thân sẽ triệt tiêu lợi ích về bộ nhớ của việc truyền theo luồng. Hãy cấu hình proxy để truyền theo luồng các phản hồi PDF, hoặc dùng phản hồi được đệm trên đường dẫn đó.
  • Callback theo luồng của Symfony. Biến thể theo luồng của Symfony trả về một StreamedResponse mà callback của nó xả output. Đừng tự ghi vào phần thân phản hồi sau khi đã trả nó về.

Việc tạo đồng bộ bên trong một controller sẽ chặn yêu cầu trong suốt quá trình dựng đầy đủ PDF. Một tài liệu một trang thường nằm gọn trong ngân sách yêu cầu điển hình. Với output nhiều trang hoặc theo lô, hãy chuyển việc tạo ra khỏi luồng yêu cầu bằng một queued job — xem Tạo một tệp PDF trong một queued job. Các biến thể theo luồng giảm bộ nhớ đỉnh cho các tài liệu lớn, đổi lại là một Content-Length không xác định. Hãy chọn chúng khi bộ nhớ là ràng buộc và không cần thanh tiến trình.

  • Các factory PdfResponse áp dụng một tập header tăng cường phản hồi cố định và làm sạch tên tệp tải xuống trong mọi tích hợp. Đừng tự thêm các header này.
  • Đừng bao giờ chèn trực tiếp dữ liệu đầu vào của người dùng chưa được xác thực vào tên tệp mà bạn truyền cho factory. Hãy truyền một giá trị bạn kiểm soát, và để factory làm sạch nó như lớp thứ hai.
  • Trong khối catch, hãy ghi log lớp ngoại lệ và một định danh tương quan, chứ không phải thông điệp hay trace của ngoại lệ. Một trace thô trong log sink là một nguồn rò rỉ thông tin.
  • Đừng bao giờ viết một khối catch rỗng. Mỗi ví dụ ở đây đều ghi log và trả về một phản hồi lỗi đã xác định.

Trang security-and-operations của mỗi tích hợp ghi lại mô hình mối đe dọa của tích hợp đó: tập header, quy tắc làm sạch tên tệp và vòng đời gắn kết tài liệu.

Hướng dẫn này không đưa ra tuyên bố tiêu chuẩn quy phạm nào. Mọi lệnh gọi API được trình bày đều là bề mặt công khai đã được xác minh của tích hợp được nêu tên, được đối chiếu chéo với các trang quickstart và production-usage của mỗi gói. Các trang production-usage thượng nguồn được liên kết trong mục Xem thêm ghi lại ngữ nghĩa header và hành vi gắn kết container mà các tích hợp dựa vào, cùng với các trích dẫn PSR của chúng. Trang cookbook này chỉ trình bày lại cách sử dụng và nhường các trích dẫn quy phạm cho những trang đó.