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

Tạo PDF bằng tác vụ hàng đợi

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 tích hợp, rồi cài đặt phụ thuộc hàng đợi mà framework của bạn cần.

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

CodeIgniter 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.

Terminal window
composer require nextpdf/codeigniter codeigniter4/queue

Vớ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 đó.

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 message readonly được điều phối trên Messenger bus, cùng với GeneratePdfHandler. Handler phân giải một builder theo tên lớp từ một PSR-11 service locator. Bạn triển khai NextPDF\Symfony\Message\PdfBuilderInterface cho 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 trong Config\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 namespace App\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.

Mối quan tâmLaravelSymfonyCodeIgniter 4
Đơn vị xếp hàngGeneratePdfJob (ShouldQueue)GeneratePdfMessage (DTO) + GeneratePdfHandlerGeneratePdfJob (queue handler)
Điều phốiGeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure)MessageBusInterface::dispatch(new GeneratePdfMessage(...))service('queue')->push($queue, $name, $data)
Hình dạng buildercallable(PdfDocumentInterface): PdfDocumentInterfacePdfBuilderInterface::build(Document, array): Documentstatic fn(Document, array): Document dưới App\PdfBuilders
Bảo vệ đường dẫn / đầu vàoTác vụ kiểm tra đường dẫn đầu ra trên workerDTO 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ạifailed() sau tries; onFailure khi thất bại dứt điểmChiến lược thử lại của Messenger; lỗi kiểm tra có kiểuInvalidArgumentException / QueueException

Dùng đoạn điều phối tối giản này trong mỗi framework.

Laravel: dispatch GeneratePdfJob
<?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.

Symfony: dispatch GeneratePdfMessage from a controller
<?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);
}
}
CodeIgniter 4: push GeneratePdfJob by its registered name
<?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.

CodeIgniter 4: 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,
];
}

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.

Laravel: app/Jobs/DispatchMonthlyStatement.php
<?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ị triesbackoff 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ý.

Symfony: src/Pdf/InvoicePdfBuilder.php
<?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;
}
}
Symfony: config/services.yaml (builder locator)
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/.

CodeIgniter 4: app/PdfBuilders/InvoiceBuilder.php
<?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.

Terminal window
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600
Terminal window
php spark queue:work pdf-queue

Hã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::class làm tên tác vụ, hàng đợi sẽ từ chối nó ngay khi đẩy. Thay vào đó, hãy đẩy khóa jobHandlers.
  • 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ào GeneratePdfJob::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ý.

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.

  • 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 catch rỗ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.

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 đó.