Truyền tệp PDF lớn đã tạo dưới dạng phản hồi HTTP
Tổng quan nhanh
Phần tiêu đề “Tổng quan nhanh”Bạn tạo một tệp PDF lớn trong controller và muốn trả về các byte mà không giữ thêm một bản sao đầy đủ trong bộ đệm phản hồi. Mỗi tích hợp framework đều cung cấp các biến thể truyền của factory PdfResponse: streamInline() và streamDownload(). Mỗi phương thức trả về một StreamedResponse của framework, kèm callback ghi phần thân PDF về client theo các phân đoạn cố định 64 KB.
Hãy đọc mô hình bộ nhớ trước khi chọn hướng đi này. Trước tiên, engine dựng toàn bộ tài liệu trong bộ nhớ. Callback truyền gọi getPdfData(), hiện thực hóa toàn bộ tệp PDF thành một chuỗi, rồi duyệt chuỗi đó theo các phân đoạn 64 KB. Bạn tiết kiệm được mức sử dụng bộ nhớ cao điểm của bản sao thứ hai mà một Illuminate\Http\Response hoặc Symfony\Component\HttpFoundation\Response dạng buffer sẽ giữ trong khi framework đo Content-Length. Biến thể truyền không đo độ dài, nên bỏ qua Content-Length. Nó không bao giờ giữ phần thân phản hồi và chuỗi tài liệu cùng một lúc. Đây không phải là truyền tăng dần thật sự: NextPDF không có bề mặt ghi tăng dần, nên tài liệu được hiện thực hóa hoàn toàn trước khi byte đầu tiên đến socket.
Trước khi bắt đầu, hãy đảm bảo các phần sau đã sẵn sàng:
- Lõi NextPDF đã được cài, và một tích hợp framework,
nextpdf/laravelhoặcnextpdf/symfony, đã được cài và phát hiện. - Bạn biết cách định tuyến một yêu cầu đến controller trong framework của mình.
- Bạn đã đọc Trả về một tệp PDF đã tạo từ một controller, bài viết trình bày các factory dạng buffer
inline()vàdownload()mà công thức này dựa trên.
Hướng dẫn này tập trung vào mẫu StreamedResponse mà Laravel và Symfony dùng chung. CodeIgniter 4 cung cấp cùng các tên phương thức streamInline() / streamDownload(), nhưng gói các byte trong một CodeIgniter\HTTP\DownloadResponse thay vì một StreamedResponse dựa trên callback. Phần Trường hợp đặc biệt sẽ nêu khác biệt đó.
Cài đặt
Phần tiêu đề “Cài đặt”Cài tích hợp dành cho framework của bạn. Chạy một trong các lệnh sau.
composer require nextpdf/laravelcomposer require nextpdf/symfonyVới Laravel, hãy xuất bản cấu hình sau khi cài đặt.
php artisan vendor:publish --tag=nextpdf-configSymfony đăng ký bundle thông qua Flex. Hãy xác nhận tích hợp đã được phát hiện trên trang cài đặt của framework trước khi tiếp tục.
Tổng quan khái niệm
Phần tiêu đề “Tổng quan khái niệm”Một factory phản hồi dạng buffer, PdfResponse::download() hoặc PdfResponse::inline(), gọi getPdfData(), lưu chuỗi trả về vào một đối tượng Response, rồi đặt Content-Length từ strlen(). Sau đó, framework giữ chuỗi đó trong suốt vòng đời của phản hồi. Với tài liệu lớn, chuỗi tài liệu và chuỗi phần thân phản hồi cùng tồn tại trong bộ nhớ trong một khoảng thời gian.
Factory dạng truyền hoạt động theo mô hình khác. PdfResponse::streamDownload() và PdfResponse::streamInline() trả về một StreamedResponse được dựng bằng callback. Framework chỉ gọi callback đó khi đã sẵn sàng gửi phần thân. Bên trong callback, tích hợp gọi getPdfData() một lần, cắt chuỗi trả về thành các phân đoạn 64 KB, rồi echo từng phân đoạn và gọi flush(). Nó không giữ lại một bản sao thường trực thứ hai của phần thân, và không phát ra header Content-Length.
Hai điểm cốt lõi chi phối mọi quyết định trên trang này:
- Dựng là eager, truyền là phân đoạn.
getPdfData()trênNextPDF\Core\Documentgọi writer và trả về toàn bộ tệp PDF dưới dạng một chuỗi. Việc phân đoạn 64 KB chỉ kiểm soát cách các byte đã dựng rời khỏi tiến trình. Bộ nhớ cao điểm bị giới hạn bởi kích thước của một tài liệu hoàn chỉnh, không phải bởi một cửa sổ truyền nhỏ. - Không có
Content-Length. Biến thể truyền không thể biết độ dài phần thân nếu không dựng nó bên trong callback, nên bỏ qua header này. Thanh tiến trình phía client, yêu cầuRange, hoặc proxy nhạy cảm với độ dài sẽ không thấy kích thước. Hãy chọndownload()/inline()dạng buffer khi độ dài đã biết quan trọng hơn việc tiết kiệm bản sao phản hồi.
Hãy lấy tài liệu qua cơ chế resolve đúng quy ước của framework:
- Laravel: resolve
NextPDF\Contracts\DocumentFactoryInterfacetừ container và gọicreate(). Nó trả về mộtNextPDF\Core\Documentmới, đúng kiểu cụ thể mà các factory dạng truyền chấp nhận. - Symfony: tiêm
NextPDF\Symfony\Service\PdfFactoryvà gọicreate(). Nó trả về mộtNextPDF\Core\Documentmới với các giá trị mặc định đã cấu hình được áp dụng.
Bề mặt API
Phần tiêu đề “Bề mặt API”| Mối quan tâm | Laravel | Symfony |
|---|---|---|
| Tài liệu mới | app(DocumentFactoryInterface::class)->create() | PdfFactory::create() |
| Truyền inline | PdfResponse::streamInline($doc, $name) | PdfResponse::streamInline($doc, $name) |
| Truyền tải xuống | PdfResponse::streamDownload($doc, $name) | PdfResponse::streamDownload($doc, $name) |
| Kiểu trả về | Symfony\Component\HttpFoundation\StreamedResponse | Symfony\Component\HttpFoundation\StreamedResponse |
| Lệnh dựng bên trong callback | NextPDF\Core\Document::getPdfData() | NextPDF\Core\Document::getPdfData() |
| Kích thước phân đoạn | 64 KB (str_split tất định) | 64 KB (vòng lặp substr tất định) |
Lớp PdfResponse của Laravel nằm tại NextPDF\Laravel\Http\PdfResponse; phiên bản của Symfony nằm tại NextPDF\Symfony\Http\PdfResponse. Các factory dạng truyền của chúng đều trả về cùng kiểu Symfony\Component\HttpFoundation\StreamedResponse. Cả hai cùng áp dụng bộ header cố định của Open Web Application Security Project (OWASP) để tăng cường bảo mật cho phản hồi (X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Content-Security-Policy: default-src 'none', X-Robots-Tag: noindex, nofollow, Referrer-Policy: no-referrer), và cả hai đều làm sạch tên tệp tải xuống. Bạn không cần tự thêm các header đó.
Cả hai factory đều gọi cùng một API lõi nền tảng, NextPDF\Core\Document::getPdfData(): string, phương thức dựng và trả về toàn bộ phần nhị phân PDF. Phương thức anh em của nó, save(string $path): void, ghi cùng các byte đó ra đĩa thông qua một writer nguyên tử. Công thức này dùng getPdfData() vì đích đến là socket HTTP, không phải một tệp.
Ví dụ mã — bắt đầu nhanh
Phần tiêu đề “Ví dụ mã — bắt đầu nhanh”Dưới đây là action truyền tải xuống tối thiểu trong mỗi framework. Các lệnh gọi tài liệu dùng cùng một API lõi; chỉ phần khung controller là khác. Factory dạng truyền trao cho framework một callback, nên action của bạn trả về ngay lập tức. Phần thân được dựng và đẩy đi khi framework gửi phản hồi.
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use NextPDF\Contracts\DocumentFactoryInterface;use NextPDF\Laravel\Http\PdfResponse;use Symfony\Component\HttpFoundation\StreamedResponse;
final class ReportController extends Controller{ public function annualReport(): StreamedResponse { $document = app(DocumentFactoryInterface::class)->create(); $document->addPage(); $document->cell(0, 10, 'Annual report', newLine: true);
return PdfResponse::streamDownload($document, 'annual-report.pdf'); }}<?php
declare(strict_types=1);
namespace App\Controller;
use NextPDF\Symfony\Http\PdfResponse;use NextPDF\Symfony\Service\PdfFactory;use Symfony\Component\HttpFoundation\StreamedResponse;use Symfony\Component\Routing\Attribute\Route;
final class ReportController{ #[Route('/report', name: 'report_pdf')] public function annualReport(PdfFactory $pdf): StreamedResponse { $document = $pdf->create(); $document->addPage(); $document->cell(0, 10, 'Annual report', newLine: true);
return PdfResponse::streamDownload($document, 'annual-report.pdf'); }}Để xem trước trong một tab trình duyệt thay vì buộc tải xuống, hãy gọi streamInline(...) thay cho streamDownload(...). Content-Disposition trở thành inline, và mọi header khác vẫn giữ nguyên.
Ví dụ mã — sản xuất
Phần tiêu đề “Ví dụ mã — sản xuất”Một action sản xuất tiêm các phụ thuộc của nó, kiểm tra đầu vào từ route, bắt ngoại lệ cụ thể nhất mà lệnh dựng có thể phát ra, ghi log lớp lỗi mà không làm rò rỉ stack trace, và trả về một lỗi Hypertext Transfer Protocol (HTTP) đã xác định. Ví dụ dưới đây dùng tiêm qua hàm khởi tạo của Laravel. Bản tương đương trong Symfony tuân theo cùng mô hình, với PdfFactory được tiêm vào từng action.
getPdfData() chạy bên trong callback dạng truyền, nên ngoại lệ do nó phát ra sẽ xuất hiện sau khi framework đã bắt đầu gửi header. Để việc xử lý lỗi vẫn hữu ích, hãy dựng tài liệu (bước có thể thất bại) trước khi bạn trao phản hồi lại, và bắt lỗi dựng tại đó. Sau đó, bên trong callback chỉ còn việc truyền các byte đã dựng theo phân đoạn.
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Response;use NextPDF\Contracts\DocumentFactoryInterface;use NextPDF\Core\Document;use NextPDF\Exception\NextPdfException;use NextPDF\Laravel\Http\PdfResponse;use Psr\Log\LoggerInterface;use Symfony\Component\HttpFoundation\StreamedResponse;
final class StatementController extends Controller{ private const int MAX_STATEMENT_ID = 9_999_999;
public function __construct( private readonly DocumentFactoryInterface $documents, private readonly LoggerInterface $logger, ) {}
public function show(int $statementId): StreamedResponse|Response { // Validate input at the boundary before any build work runs. if ($statementId < 1 || $statementId > self::MAX_STATEMENT_ID) { return new Response('Invalid statement identifier.', 422); }
try { // Build the whole document up front. getPdfData(), invoked inside // the streamed callback, materializes the full PDF in memory, so // do the failure-prone build here, where the catch can still set a // clean HTTP status before any byte is sent. $document = $this->buildStatement($statementId); $document->getPdfData(); } catch (NextPdfException $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('Statement PDF build failed', [ 'statement_id' => $statementId, 'exception' => $exception::class, ]);
return new Response('Could not generate the statement PDF.', 500); }
// The build succeeded. The streamed factory rebuilds the bytes inside // its callback and flushes them to the client in 64 KB chunks. return PdfResponse::streamDownload( $document, "statement-{$statementId}.pdf", ); }
private function buildStatement(int $statementId): Document { $document = $this->documents->create(); $document->addPage(); $document->cell(0, 10, "Statement #{$statementId}", newLine: true);
return $document; }}Hãy bắt NextPDF\Exception\NextPdfException, lớp cơ sở trừu tượng mà mọi ngoại lệ NextPDF đều kế thừa, khi bạn muốn có một trình xử lý chung cho mọi lỗi dựng. Để phản ứng với nguyên nhân cụ thể, trước tiên hãy bắt các kiểu con cụ thể mà getPdfData() có thể phát ra: NextPDF\Exception\PageLayoutException khi nội dung không vừa với hình học trang, NextPDF\Exception\CompressionException khi nén luồng thất bại, và NextPDF\Exception\InvalidConfigException cho cấu hình đầu ra không hợp lệ. Đừng bao giờ viết khối catch rỗng. Mỗi nhánh ở đây đều ghi log lớp lỗi và trả về một trạng thái đã xác định.
Việc resolve một tài liệu mới cho mỗi action giúp factory có thể thay thế được trong kiểm thử. Đừng tái sử dụng một thực thể 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, vì trạng thái nội dung cũ sẽ bị mang theo.
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 được dựng hai lần trong mẫu kiểm-tra-rồi-truyền. Ví dụ sản xuất gọi
getPdfData()một lần để kiểm tra lệnh dựng, sau đó factory gọi lại bên trong callback. Đây là cái giá của việc dời điểm thất bại lên trước các header. Khi việc dựng hai lần quá tốn kém cho một tài liệu cụ thể, hãy bỏ qua bước dựng thăm dò trước và chấp nhận rằng lỗi dựng bên trong callback sẽ làm cụt một phản hồi đã bắt đầu. - Không có
Content-Length. Biến thể truyền bỏ qua header này. Các thanh tiến trình tải xuống và yêu cầuRangesẽ không hoạt động. Hãy dùngdownload()/inline()dạng buffer khi cần độ dài đã biết. - Proxy có buffer làm mất đi lợi ích. Reverse proxy hoặc bộ đệm đầu ra PHP gom toàn bộ phần thân trước khi chuyển tiếp sẽ lại giữ toàn bộ tệp PDF, xóa bỏ lợi ích từ bản sao đã tiết kiệm. Hãy cấu hình proxy để truyền các phản hồi
application/pdf, hoặc dùng phản hồi dạng buffer trong luồng đó. - CodeIgniter 4 không truyền theo callback. Tích hợp CodeIgniter cung cấp cùng các tên phương thức
streamInline()/streamDownload(), nhưng chúng trả về mộtCodeIgniter\HTTP\DownloadResponsegiữ toàn bộ phần thân, không phải mộtStreamedResponsedựa trên callback. Mẫu StreamedResponse trên trang này chỉ áp dụng cho Laravel và Symfony. - Đừng ghi vào phần thân sau khi đã trả về. Callback dạng truyền sở hữu đầu ra. Đừng tự
echohoặc ghi vào phần thân phản hồi sau khi bạn đã trảStreamedResponselại cho framework. - Tài liệu đã ký sẽ thất bại sớm. Việc gọi
getPdfData()trên một tài liệu được thiết lập cho chữ ký PAdES cấp cao sẽ phát raNextPDF\Exception\NotImplementedExceptionthay vì phát ra một tệp chưa ký. Hãy truyền đầu ra đã ký qua đường ký đã được tài liệu hóa, không phải qua công thức này.
Hiệu năng
Phần tiêu đề “Hiệu năng”Việc truyền chỉ giới hạn bản sao phản hồi, không giới hạn lệnh dựng tài liệu. Bộ nhớ cao điểm xấp xỉ kích thước của một tệp PDF hoàn chỉnh, vì getPdfData() hiện thực hóa toàn bộ tài liệu trước khi gửi phân đoạn đầu tiên. Với tài liệu thực sự lớn hoặc nhiều trang, chính lệnh dựng, chứ không phải việc truyền, chiếm phần lớn ngân sách yêu cầu. Hãy dời việc tạo ra khỏi luồng yêu cầu bằng một công việc xếp hàng. Xem Tạo một tệp PDF trong một công việc xếp hàng.
Kích thước phân đoạn 64 KB là cố định và tất định trong cả hai tích hợp. Nó chỉ kiểm soát mức độ chi tiết của việc truyền, không thay đổi tổng số byte được gửi hay bộ nhớ cao điểm. Hãy chọn biến thể truyền khi bản sao phản hồi tiết kiệm được là yếu tố ràng buộc và bạn không cần thanh tiến trình. Hãy chọn biến thể buffer cho các phản hồi nhỏ, nhạy cảm với độ trễ, hưởng lợi từ một Content-Length đã biết.
Lưu ý bảo mật
Phần tiêu đề “Lưu ý bảo mật”- Kiểm tra đầu vào trước khi dựng. Action sản xuất từ chối định danh ngoài phạm vi bằng một
422trước khi bất kỳ công việc dựng nào chạy. Đừng bao giờ đưa đầu vào chưa kiểm tra vào lệnh dựng hay tên tệp. - Việc làm sạch tên tệp đã được áp dụng sẵn. Cả hai factory dạng truyền đều làm sạch tên tệp và thêm bộ header tăng cường bảo mật phản hồi của OWASP. Hãy truyền vào giá trị bạn kiểm soát và để factory làm sạch nó như một lớp thứ hai. Đừng tự tay mã hóa tên tệp.
- Giới hạn bộ nhớ đồng thời. Vì toàn bộ tệp PDF được hiện thực hóa trong bộ nhớ cho mỗi yêu cầu, lưu lượng đồng thời cao sẽ nhân lên bộ nhớ cao điểm. Hãy áp đặt giới hạn kích thước và tốc độ cho các đầu vào điều khiển lệnh dựng để giảm thiểu tấn công từ chối dịch vụ bằng cách làm cạn kiệt bộ nhớ.
- Ghi log lớp lỗi, không phải thông điệp. Khối catch ghi log
$exception::classvà một định danh tương quan, không bao giờ ghi thông điệp ngoại lệ hay stack trace. Stack trace thô trong bộ thu log là một rò rỉ thông tin. - Không có catch rỗng. Mọi nhánh catch trên trang này đều ghi log và trả về phản hồi lỗi đã xác định.
Tuân thủ
Phần tiêu đề “Tuân thủ”Hướng dẫn này không đưa ra tuyên bố tuân thủ tiêu chuẩn quy phạm nào. Mọi lớp, phương thức và header đượ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: NextPDF\Core\Document::getPdfData(), các factory dạng truyền NextPDF\Laravel\Http\PdfResponse và NextPDF\Symfony\Http\PdfResponse, và kiểu trả về Symfony\Component\HttpFoundation\StreamedResponse. Ngữ nghĩa của bộ header OWASP tăng cường bảo mật cho phản hồi mà các factory áp dụng được lập thành tài liệu, kèm trích dẫn, trên trang bảo mật-và-vận hành của mỗi tích hợp được liên kết trong mục Xem thêm. Trang cookbook này chỉ nêu lại cách sử dụng và dành các trích dẫn quy phạm cho những trang đó.
Xem thêm
Phần tiêu đề “Xem thêm”- Trả về một tệp PDF đã tạo từ một controller: các đối ứng dạng buffer
inline()vàdownload(). - Tạo một tệp PDF trong một công việc xếp hàng: dời lệnh dựng ra khỏi luồng yêu cầu.
- Sử dụng trong sản xuất với Laravel: controller được nối qua DI, bộ header OWASP, và hợp đồng ràng buộc container.
- Sử dụng trong sản xuất với Symfony: callback dạng truyền, bộ phát phân đoạn 64 KB, và bộ định vị builder.