Tạo PDF bằng tác vụ hàng đợi
Tổng quan nhanh
Phần tiêu đề “Tổng quan nhanh”Không nên chạy quá trình tạo PDF nặng trên luồng yêu cầu. Mỗi tích hợp framework cung cấp một API tạo theo hàng đợi để dựng và lưu PDF trên worker. Yêu cầu HTTP có thể trả về ngay sau khi bạn điều phối công việc. Hướng dẫn này trình bày luồng hàng đợi cho Laravel (GeneratePdfJob), Symfony (GeneratePdfMessage qua Messenger) và CodeIgniter 4 (GeneratePdfJob thông qua codeigniter4/queue).
Các điều kiện tiên quyết là:
- Đã cài đặt NextPDF core và một tích hợp framework.
- Đã cấu hình một worker transport: queue connection của Laravel, Messenger transport của Symfony, hoặc hàng đợi CodeIgniter 4 với
codeigniter4/queueđã được cài đặt. - Một tiến trình worker đang chạy cho transport đó.
Hướng dẫn này giả định ứng dụng của bạn đã có sẵn hàng đợi. Để thiết lập hàng đợi hoặc Messenger, hãy dùng tài liệu riêng của framework bạn đang dùng.
Cài đặt
Phần tiêu đề “Cài đặt”Cài đặt tích hợp, rồi cài đặt phụ thuộc hàng đợi mà framework của bạn cần.
composer require nextpdf/laravelcomposer require nextpdf/symfony symfony/messengerCodeIgniter cần gói hàng đợi. Tích hợp này khai báo gói đó là phụ thuộc chỉ dành cho phát triển, vì vậy hãy require nó trong ứng dụng chạy worker.
composer require nextpdf/codeigniter codeigniter4/queueVới Laravel, hãy cấu hình queue connection trong config/nextpdf.php (queue.connection, queue.queue, queue.timeout), rồi chạy một worker cho connection đó.
Tổng quan khái niệm
Phần tiêu đề “Tổng quan khái niệm”Mỗi tích hợp dùng cùng một mẫu, theo phong cách riêng của framework:
- Laravel cung cấp
NextPDF\Laravel\Jobs\GeneratePdfJob, một tác vụShouldQueue. Bạn điều phối tác vụ này cùng một đường dẫn đầu ra và một builder closure. Closure nhận một document được container phân giải và trả về document đã được cấu hình. Trên worker, tác vụ lưu document được trả về vào đường dẫn. Tác vụ cũng chấp nhận các callback thành công và thất bại tùy chọn. - Symfony cung cấp
NextPDF\Symfony\Message\GeneratePdfMessage, một messagereadonlyđược điều phối trên Messenger bus, cùng vớiGeneratePdfHandler. Handler phân giải một builder theo tên lớp từ một PSR-11 service locator. Bạn triển khaiNextPDF\Symfony\Message\PdfBuilderInterfacecho từng loại document. - CodeIgniter 4 cung cấp
NextPDF\CodeIgniter\Jobs\GeneratePdfJob, được đăng ký dưới một khóa đã đặt tên trongConfig\Queue::$jobHandlers. Bạn đẩy tác vụ bằng tên đã đăng ký của nó cùng với một tham chiếu builder, một đường dẫn đầu ra và một mảng context. Builder là phương thức tĩnh được giới hạn trong namespaceApp\PdfBuilders.
Cả ba tích hợp đều có cùng lập trường bảo mật: chúng kiểm tra đường dẫn đầu ra. Symfony và CodeIgniter kiểm tra lại đường dẫn này tại thời điểm tiêu thụ, vì một payload có thể chờ trong hàng đợi từ lúc điều phối đến lúc thực thi. Builder chạy với một document mới trên worker, nên các tác vụ song song không bao giờ chia sẻ trạng thái document.
Bề mặt API
Phần tiêu đề “Bề mặt API”| Mối quan tâm | Laravel | Symfony | CodeIgniter 4 |
|---|---|---|---|
| Đơn vị xếp hàng | GeneratePdfJob (ShouldQueue) | GeneratePdfMessage (DTO) + GeneratePdfHandler | GeneratePdfJob (queue handler) |
| Điều phối | GeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure) | MessageBusInterface::dispatch(new GeneratePdfMessage(...)) | service('queue')->push($queue, $name, $data) |
| Hình dạng builder | callable(PdfDocumentInterface): PdfDocumentInterface | PdfBuilderInterface::build(Document, array): Document | static fn(Document, array): Document dưới App\PdfBuilders |
| Bảo vệ đường dẫn / đầu vào | Tác vụ kiểm tra đường dẫn đầu ra trên worker | DTO kiểm tra lúc khởi tạo, handler kiểm tra lại lúc tiêu thụ | Tác vụ giới hạn đường dẫn trong WRITEPATH/pdfs/, đưa namespace của builder vào danh sách cho phép |
| Bề mặt thất bại | failed() sau tries; onFailure khi thất bại dứt điểm | Chiến lược thử lại của Messenger; lỗi kiểm tra có kiểu | InvalidArgumentException / QueueException |
Mẫu mã — bắt đầu nhanh
Phần tiêu đề “Mẫu mã — bắt đầu nhanh”Dùng đoạn điều phối tối giản này trong mỗi framework.
<?php
declare(strict_types=1);
use NextPDF\Contracts\PdfDocumentInterface;use NextPDF\Laravel\Jobs\GeneratePdfJob;
GeneratePdfJob::dispatch( storage_path('app/reports/january-2026.pdf'), static fn (PdfDocumentInterface $document): PdfDocumentInterface => $document ->addPage() ->cell(0, 10, 'January report', newLine: true),);Đường dẫn đầu ra phải kết thúc bằng .pdf; tác vụ kiểm tra đường dẫn trên worker trước khi ghi tệp.
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Pdf\InvoicePdfBuilder;use NextPDF\Symfony\Message\GeneratePdfMessage;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Messenger\MessageBusInterface;use Symfony\Component\Routing\Attribute\Route;
final class ReportController{ #[Route('/invoice/{id}/queue', name: 'invoice_queue')] public function queue(MessageBusInterface $bus, int $id): Response { $bus->dispatch(new GeneratePdfMessage( builderClass: InvoicePdfBuilder::class, outputPath: '/var/storage/invoices/' . $id . '.pdf', builderContext: ['invoice_id' => $id], ));
return new Response('PDF generation queued.', 202); }}<?php
declare(strict_types=1);
namespace App\Controllers;
use CodeIgniter\HTTP\ResponseInterface;
final class InvoiceController extends BaseController{ public function queueInvoice(int $id): ResponseInterface { service('queue')->push('pdf-queue', 'generate-pdf', [ 'builder' => 'App\\PdfBuilders\\InvoiceBuilder::build', 'outputPath' => WRITEPATH . 'pdfs/invoice-' . $id . '.pdf', 'context' => ['invoice_id' => $id], ]);
return $this->response ->setStatusCode(ResponseInterface::HTTP_ACCEPTED) ->setJSON(['status' => 'queued', 'invoice_id' => $id]); }}Trong CodeIgniter, hãy đẩy khóa jobHandlers ('generate-pdf'), không phải chuỗi tên lớp của tác vụ. Hãy đăng ký handler trước ở app/Config/Queue.php.
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Queue\Config\Queue as BaseQueue;use NextPDF\CodeIgniter\Jobs\GeneratePdfJob;
final class Queue extends BaseQueue{ /** @var array<string, class-string> */ public array $jobHandlers = [ 'generate-pdf' => GeneratePdfJob::class, ];}Mẫu mã — sản xuất
Phần tiêu đề “Mẫu mã — sản xuất”Trong môi trường sản xuất, đoạn điều phối sẽ nối callback thành công và thất bại (Laravel), hoặc builder được đăng ký tường minh cùng handler có kiểu (Symfony), với PSR-3 logger. Ví dụ Laravel bên dưới điều phối cùng cả hai callback.
<?php
declare(strict_types=1);
namespace App\Jobs;
use NextPDF\Contracts\PdfDocumentInterface;use NextPDF\Laravel\Jobs\GeneratePdfJob;use Psr\Log\LoggerInterface;use Throwable;
final class DispatchMonthlyStatement{ public function __construct(private readonly LoggerInterface $logger) {}
public function __invoke(int $accountId): void { // dispatch() is public static: it constructs the job from the // arguments it receives. Pass every argument — including the // callbacks — to the static call, not to a separately built instance. GeneratePdfJob::dispatch( storage_path("app/statements/{$accountId}.pdf"), static fn (PdfDocumentInterface $document): PdfDocumentInterface => $document ->addPage() ->cell(0, 10, "Statement for account {$accountId}", newLine: true), function (string $path) use ($accountId): void { $this->logger->info('Statement PDF written', [ 'account_id' => $accountId, 'path' => $path, ]); }, function (Throwable $exception) use ($accountId): void { $this->logger->error('Statement PDF failed', [ 'account_id' => $accountId, 'exception' => $exception::class, ]); }, ); }}Callback thành công nhận đường dẫn đầu ra. Callback thất bại nhận Throwable. Tác vụ dùng hết số lần tries (mặc định 3) trước khi luồng thất bại chạy. Tinh chỉnh timeout thông qua nextpdf.queue.timeout. Các giá trị tries và backoff là thuộc tính public, vì vậy hãy kế thừa GeneratePdfJob để thay đổi chúng.
Với Symfony, hãy triển khai builder và đăng ký nó trong một service locator. Cách này giữ cho handler chỉ làm việc với các builder đã đăng ký.
<?php
declare(strict_types=1);
namespace App\Pdf;
use NextPDF\Core\Document;use NextPDF\Symfony\Message\PdfBuilderInterface;
final class InvoicePdfBuilder implements PdfBuilderInterface{ /** @param array<string, mixed> $context */ public function build(Document $document, array $context): Document { $document->addPage(); $document->setFont('dejavusans', '', 12); $document->cell(0, 10, 'Invoice #' . $context['invoice_id']);
return $document; }}services: App\Pdf\InvoicePdfBuilder: ~
nextpdf.pdf_builder_locator: class: Symfony\Component\DependencyInjection\ServiceLocator arguments: - 'App\Pdf\InvoicePdfBuilder': '@App\Pdf\InvoicePdfBuilder' tags: ['container.service_locator']
NextPDF\Symfony\Message\GeneratePdfHandler: arguments: $builderLocator: '@nextpdf.pdf_builder_locator'Với CodeIgniter, hãy triển khai builder như một phương thức tĩnh dưới App\PdfBuilders. Tác vụ từ chối mọi tham chiếu builder nằm ngoài namespace đó và mọi đường dẫn đầu ra nằm ngoài WRITEPATH/pdfs/.
<?php
declare(strict_types=1);
namespace App\PdfBuilders;
use NextPDF\Core\Document;
final class InvoiceBuilder{ /** @param array<string, mixed> $context */ public static function build(Document $document, array $context): Document { $invoiceId = (int) ($context['invoice_id'] ?? 0);
$document->addPage(); $document->cell(0, 10, "Invoice #{$invoiceId}");
return $document; }}Chạy worker cho mỗi framework.
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600php spark queue:work pdf-queueHãy tái khởi động worker của Laravel và Symfony theo vòng đời có giới hạn (--limit / --memory-limit / --time-limit) để vùng cấp phát bị rò rỉ trong một phụ thuộc không thể phình to vô hạn.
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 ý”- Giá trị do builder trả về chính là thứ được lưu. Trong mọi tích hợp, worker lưu document mà builder trả về, không phải instance ban đầu mà nó đã phân giải. Hãy luôn trả về document đã được cấu hình từ builder.
- Việc kiểm tra đường dẫn chạy trên worker. Symfony kiểm tra đường dẫn đầu ra lúc khởi tạo và một lần nữa tại thời điểm tiêu thụ. CodeIgniter giới hạn đường dẫn trong
WRITEPATH/pdfs/và từ chối các đường dẫn vượt cấp cũng như đường dẫn ở thư mục anh em có tiền tố trùng. Một đường dẫn an toàn lúc điều phối nhưng không an toàn lúc tiêu thụ vẫn bị từ chối. - CodeIgniter đẩy tên, không phải lớp. Nếu bạn đẩy
GeneratePdfJob::classlàm tên tác vụ, hàng đợi sẽ từ chối nó ngay khi đẩy. Thay vào đó, hãy đẩy khóajobHandlers. - Các callback của Laravel phải được truyền vào lệnh dispatch tĩnh. Nếu bạn dựng một instance tác vụ rồi gọi
$job->dispatch(...), lệnh gọi đó bỏ qua instance và các callback của nó. Hãy truyền các callback vàoGeneratePdfJob::dispatch(...). - Các registry an toàn cho worker. Font registry là một singleton bị khóa với vòng đời theo tiến trình, còn image registry là một cache có giới hạn. Document được tạo mới cho mỗi tác vụ. Đừng yêu cầu dùng chung một document trên worker.
- Ký trong worker. Đầu ra đã ký hoặc PDF/A trong tác vụ hàng đợi yêu cầu một phiên bản NextPDF thương mại được cài đặt trong môi trường worker. Nếu không có phiên bản này, dịch vụ ký phân giải thành
null. Hãy kiểm tra null trước khi ký.
Hiệu năng
Phần tiêu đề “Hiệu năng”Chuyển khâu tạo sang tác vụ hàng đợi sẽ loại bỏ toàn bộ thời gian dựng PDF khỏi yêu cầu HTTP. Yêu cầu trả về ngay khi công việc được điều phối. Font registry và image registry phân bổ chi phí thiết lập của chúng trong suốt vòng đời worker, nên chi phí cho mỗi tác vụ chỉ giới hạn ở việc dựng document và phát nội dung. Hãy điều chỉnh số lượng tác vụ đang xử lý cho phù hợp với pool worker của bạn, và nạp sẵn preload_fonts (Laravel, Symfony) để quá trình khởi động phông chữ chỉ diễn ra một lần khi worker khởi động thay vì ở tác vụ đầu tiên.
Lưu ý bảo mật
Phần tiêu đề “Lưu ý bảo mật”- Payload của hàng đợi có thể bị kẻ tấn công kiểm soát nếu broker bị truy cập, vì vậy hãy coi đường dẫn đầu ra và tham chiếu builder trong một payload là không đáng tin. Các tích hợp thực thi điều này bằng việc kiểm tra đường dẫn và, ở CodeIgniter, một danh sách cho phép namespace của builder.
- Hãy giới hạn quyền hệ thống tệp của worker vào đúng thư mục đầu ra dự kiến như một lớp phòng thủ theo chiều sâu. Nếu một đường dẫn bị giả mạo bằng cách nào đó vượt qua được khâu kiểm tra, nó vẫn không thể thoát khỏi thư mục.
- Trong callback thất bại, hãy ghi log lớp ngoại lệ và một định danh tương quan, đừng bao giờ ghi thông điệp hay vết stack.
- Đừng bao giờ viết một khối
catchrỗng. Mọi callback thất bại ở đây đều ghi log và mang theo context.
Trang security-and-operations của từng tích hợp trình bày toàn bộ mô hình mối đe dọa của hàng đợi: kiểm tra payload, danh sách cho phép callable và giới hạn đường dẫn.
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ệnh gọi API được trình bày đều thuộc bề mặt công khai đã được kiểm chứng của tích hợp được nêu tên. Luồng hàng đợi dựa trên các bảo đảm về container-binding: một document mới cho mỗi lần phân giải và font registry bị khóa. Các trang production-usage thượng nguồn được liên kết dưới mục Xem thêm ghi lại những bảo đảm đó cùng với các trích dẫn PSR của chúng. Trang cookbook này chỉ nêu lại cách dùng và dành phần trích dẫn cho những trang đó.
Xem thêm
Phần tiêu đề “Xem thêm”- Trả về PDF đã tạo từ controller — phiên bản đồng bộ tương ứng.
- Sử dụng Laravel trong môi trường sản xuất —
GeneratePdfJob, các callback và bảng tinh chỉnh hàng đợi. - Sử dụng Symfony trong môi trường sản xuất — an toàn worker của Messenger và builder locator.
- Sử dụng CodeIgniter trong môi trường sản xuất —
GeneratePdfJob,jobHandlersvà giới hạn đường dẫn.