สร้าง PDF ในงานที่เข้าคิว
ภาพรวมโดยสังเขป
หัวข้อที่มีชื่อว่า “ภาพรวมโดยสังเขป”การสร้าง PDF ที่ใช้ทรัพยากรมากไม่ควรทำงานบนเธรดคำขอ framework integration แต่ละตัวมี API สำหรับการสร้างแบบเข้าคิว ซึ่งจะสร้างและบันทึก PDF บน worker ให้คุณ คำขอ HTTP สามารถตอบกลับได้ทันทีหลังจากคุณ dispatch งาน คู่มือนี้ครอบคลุมเส้นทางแบบเข้าคิวสำหรับ Laravel (GeneratePdfJob), Symfony (GeneratePdfMessage ผ่าน Messenger) และ CodeIgniter 4 (GeneratePdfJob ผ่าน codeigniter4/queue)
สิ่งที่ต้องมีก่อนมีดังนี้:
- ติดตั้ง core ของ NextPDF พร้อม framework integration หนึ่งตัวแล้ว
- ตั้งค่า worker transport แล้ว: queue connection ของ Laravel, Messenger transport ของ Symfony หรือ queue ของ CodeIgniter 4 ที่ติดตั้ง
codeigniter4/queueไว้ - มีกระบวนการ worker ทำงานอยู่สำหรับ transport นั้น
คู่มือนี้ถือว่าแอปพลิเคชันของคุณมี queue อยู่แล้ว สำหรับการตั้งค่า queue หรือ Messenger โปรดใช้เอกสารของ framework นั้นโดยตรง
การติดตั้ง
หัวข้อที่มีชื่อว่า “การติดตั้ง”ติดตั้ง integration แล้วติดตั้ง queue dependency ที่ framework ของคุณต้องใช้
composer require nextpdf/laravelcomposer require nextpdf/symfony symfony/messengerCodeIgniter ต้องใช้แพ็กเกจ queue โดย integration ประกาศแพ็กเกจนี้เป็น dependency สำหรับการพัฒนาเท่านั้น ดังนั้นให้ require แพ็กเกจในแอปพลิเคชันที่รัน worker
composer require nextpdf/codeigniter codeigniter4/queueสำหรับ Laravel ให้ตั้งค่า queue connection ใน config/nextpdf.php (queue.connection queue.queue queue.timeout) จากนั้นให้รัน worker สำหรับ connection นั้น
ภาพรวมเชิงแนวคิด
หัวข้อที่มีชื่อว่า “ภาพรวมเชิงแนวคิด”integration แต่ละตัวใช้รูปแบบเดียวกันตามแนวทางของ framework ของตน:
- Laravel มาพร้อมกับ
NextPDF\Laravel\Jobs\GeneratePdfJobซึ่งเป็นงานแบบShouldQueueคุณ dispatch งานนี้พร้อม output path และ closure ของ builder โดย closure จะได้รับ document ที่ container resolve ให้ และส่งคืน document ที่ตั้งค่าแล้ว เมื่อรันบน worker งานจะบันทึก document ที่ส่งคืนลงในเส้นทางนั้น งานยังรับ callback เสริมสำหรับความสำเร็จและความล้มเหลวได้ด้วย - Symfony มาพร้อมกับ
NextPDF\Symfony\Message\GeneratePdfMessageซึ่งเป็น message แบบreadonlyที่ dispatch บน Messenger bus พร้อมกับGeneratePdfHandlerhandler จะ resolve builder ตามชื่อคลาสจาก PSR-11 service locator คุณต้อง implementNextPDF\Symfony\Message\PdfBuilderInterfaceสำหรับ document แต่ละประเภท - CodeIgniter 4 มาพร้อมกับ
NextPDF\CodeIgniter\Jobs\GeneratePdfJobซึ่งลงทะเบียนภายใต้คีย์ชื่อในConfig\Queue::$jobHandlersคุณ push งานด้วยชื่อที่ลงทะเบียนไว้ พร้อมกับ reference ของ builder, output path และ context array builder เป็น static method ที่จำกัดอยู่ในApp\PdfBuildersnamespace
integration ทั้งสามตัวมีหลักความปลอดภัยร่วมกันหนึ่งอย่าง: ตรวจสอบ output path Symfony และ CodeIgniter ตรวจสอบ output path ซ้ำตอน consume เพราะ payload อาจรออยู่ใน queue ระหว่าง dispatch และ execute builder ทำงานกับ document ใหม่บน worker ดังนั้นงานที่ทำงานพร้อมกันจึงไม่แชร์สถานะของ document กัน
พื้นผิว API
หัวข้อที่มีชื่อว่า “พื้นผิว API”| ประเด็น | Laravel | Symfony | CodeIgniter 4 |
|---|---|---|---|
| หน่วยงานที่เข้าคิว | GeneratePdfJob (ShouldQueue) | GeneratePdfMessage (DTO) + GeneratePdfHandler | GeneratePdfJob (queue handler) |
| การ dispatch | GeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure) | MessageBusInterface::dispatch(new GeneratePdfMessage(...)) | service('queue')->push($queue, $name, $data) |
| รูปแบบของ builder | callable(PdfDocumentInterface): PdfDocumentInterface | PdfBuilderInterface::build(Document, array): Document | static fn(Document, array): Document ภายใต้ App\PdfBuilders |
| การป้องกันเส้นทาง / อินพุต | งานตรวจสอบ output path บน worker | DTO ตรวจสอบตอนสร้าง และ handler ตรวจสอบซ้ำตอน consume | งานจำกัดเส้นทางไว้ที่ WRITEPATH/pdfs/ และอนุญาตเฉพาะ namespace ของ builder ตาม allowlist |
| จุดที่เกิดความล้มเหลว | failed() หลังจากครบ tries; onFailure เมื่อล้มเหลวขั้นสุดท้าย | กลยุทธ์ retry ของ Messenger; ข้อผิดพลาดจากการตรวจสอบที่มี type | InvalidArgumentException / QueueException |
ตัวอย่างโค้ด — เริ่มต้นอย่างรวดเร็ว
หัวข้อที่มีชื่อว่า “ตัวอย่างโค้ด — เริ่มต้นอย่างรวดเร็ว”ใช้รูปแบบ dispatch ขั้นต่ำต่อไปนี้ในแต่ละ 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),);output path ต้องลงท้ายด้วย .pdf; งานจะตรวจสอบเส้นทางบน worker ก่อนเขียนไฟล์
<?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]); }}ใน CodeIgniter ให้ push คีย์ของ jobHandlers ('generate-pdf') ไม่ใช่สตริงคลาสของงาน ให้ลงทะเบียน handler ใน 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, ];}ตัวอย่างโค้ด — การใช้งานจริง
หัวข้อที่มีชื่อว่า “ตัวอย่างโค้ด — การใช้งานจริง”ในการใช้งานจริง การ dispatch จะผูก callback สำหรับความสำเร็จและความล้มเหลว (Laravel) หรือ builder ที่ลงทะเบียนไว้อย่างชัดเจนและ handler ที่มี type (Symfony) เข้ากับ PSR-3 logger ตัวอย่าง Laravel ด้านล่างนี้ dispatch พร้อม 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 สำหรับความสำเร็จจะได้รับ output path ส่วน callback สำหรับความล้มเหลวจะได้รับ Throwable งานจะพยายามทำงานจนครบ tries (ค่าเริ่มต้น 3) ก่อนที่เส้นทางความล้มเหลวจะทำงาน ปรับ timeout ผ่าน nextpdf.queue.timeout ค่า tries และ backoff เป็น public property ดังนั้นให้ subclass GeneratePdfJob หากต้องการเปลี่ยนค่าเหล่านี้
สำหรับ Symfony ให้ implement builder และลงทะเบียนใน service locator วิธีนี้ทำให้ handler จำกัดอยู่เฉพาะ builder ที่ลงทะเบียนไว้
<?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'สำหรับ CodeIgniter ให้ implement builder เป็น static method ภายใต้ App\PdfBuilders งานจะปฏิเสธ reference ของ builder ใดๆ ที่อยู่นอก namespace นั้น และ output path ใดๆ ที่อยู่นอก 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; }}รัน worker สำหรับแต่ละ framework
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600php spark queue:work pdf-queueรีไซเคิล worker ของ Laravel และ Symfony โดยกำหนดอายุการทำงานให้จำกัด (--limit / --memory-limit / --time-limit) เพื่อไม่ให้ allocation ที่รั่วจาก dependency เติบโตได้อย่างไร้ขีดจำกัด
กรณีขอบและข้อควรระวัง
หัวข้อที่มีชื่อว่า “กรณีขอบและข้อควรระวัง”- ค่าที่ builder ส่งคืนคือสิ่งที่ถูกบันทึก ในทุก integration worker จะบันทึก document ที่ builder ส่งคืน ไม่ใช่ instance ที่ resolve มาในตอนแรก ให้ส่งคืน document ที่ตั้งค่าแล้วจาก builder เสมอ
- การตรวจสอบเส้นทางทำงานบน worker Symfony ตรวจสอบ output path ตอนสร้างและตรวจสอบซ้ำตอน consume CodeIgniter จำกัดเส้นทางไว้ที่
WRITEPATH/pdfs/และปฏิเสธเส้นทางแบบ traversal และแบบ sibling-prefix เส้นทางที่ปลอดภัยตอน dispatch แต่ไม่ปลอดภัยตอน consume ก็ยังถูกปฏิเสธ - CodeIgniter push ชื่อ ไม่ใช่คลาส หากคุณ push
GeneratePdfJob::classเป็นชื่อของงาน queue จะปฏิเสธตอน push ให้ push คีย์ของjobHandlersแทน - callback ของ Laravel ต้องส่งไปยัง static dispatch หากคุณสร้าง instance ของงานแล้วเรียก
$job->dispatch(...)การเรียกนั้นจะทิ้ง instance และ callback ของมันไป ให้ส่ง callback ไปยังGeneratePdfJob::dispatch(...)แทน - registry ที่ปลอดภัยสำหรับ worker font registry เป็น singleton ที่ถูกล็อกตลอดอายุของกระบวนการ และ image registry เป็น cache ที่มีขอบเขตจำกัด document เป็นของใหม่ในแต่ละงาน อย่าขอ document ที่แชร์กันบน worker
- การลงนามใน worker การส่งออกแบบลงนามหรือ PDF/A ในงาน queue ต้องใช้ NextPDF รุ่นเชิงพาณิชย์ที่ติดตั้งในสภาพแวดล้อมของ worker หากไม่มี service การลงนามจะ resolve เป็น
nullให้ตรวจสอบ null ก่อนลงนาม
ประสิทธิภาพ
หัวข้อที่มีชื่อว่า “ประสิทธิภาพ”การย้ายการสร้างไปยังงานแบบเข้าคิวจะย้ายเวลาทั้งหมดในการ build PDF ออกจากคำขอ HTTP คำขอจะตอบกลับได้หลังจาก dispatch งานแล้ว font และ image registry ช่วยเฉลี่ยต้นทุนการตั้งค่าตลอดอายุของ worker ดังนั้นต้นทุนต่อหนึ่งงานจึงเหลือเพียงการสร้าง document และการ emit เนื้อหา กำหนดจำนวนงานพร้อมกันให้เหมาะกับ worker pool ของคุณ และกำหนด preload_fonts ไว้ล่วงหน้า (Laravel, Symfony) เพื่อให้การ warmup ฟอนต์เกิดขึ้นครั้งเดียวตอน worker boot แทนที่จะเกิดในงานแรก
หมายเหตุด้านความปลอดภัย
หัวข้อที่มีชื่อว่า “หมายเหตุด้านความปลอดภัย”- ผู้โจมตีสามารถแทรกแซง payload ของ queue ได้หากเข้าถึง broker ได้ ดังนั้นให้ถือว่า output path และ reference ของ builder ใน payload เป็นสิ่งที่ไม่น่าเชื่อถือ integration บังคับใช้สิ่งนี้ด้วยการตรวจสอบเส้นทาง และใน CodeIgniter ด้วย allowlist ของ namespace ของ builder
- จำกัดสิทธิ์ระบบไฟล์ของ worker ไว้เฉพาะไดเรกทอรีปลายทางที่ตั้งใจไว้ เพื่อเป็นการป้องกันเชิงลึก แม้เส้นทางที่ถูกดัดแปลงจะผ่านการตรวจสอบไปได้ด้วยเหตุใดก็ตาม เส้นทางนั้นก็ยังออกนอกไดเรกทอรีไม่ได้
- บันทึกคลาสของ exception และตัวระบุ correlation ไว้ใน callback สำหรับความล้มเหลว ห้ามบันทึกข้อความหรือ trace
- อย่าเขียนบล็อก
catchที่ว่างเปล่าเด็ดขาด callback สำหรับความล้มเหลวทุกตัวในที่นี้บันทึก log และแนบ context ไปด้วย
หน้า security-and-operations ของแต่ละ integration ครอบคลุม threat model ของ queue ทั้งหมด ได้แก่ การตรวจสอบ payload, allowlist ของ callable และการจำกัดเส้นทาง
ความสอดคล้อง
หัวข้อที่มีชื่อว่า “ความสอดคล้อง”คู่มือนี้ไม่ได้อ้างอิงมาตรฐานเชิงบรรทัดฐานใดๆ การเรียก API ทุกครั้งที่แสดงคือพื้นผิวสาธารณะที่ได้รับการยืนยันของ integration ที่ระบุชื่อ เส้นทางแบบเข้าคิวพึ่งพาการรับประกันของ container binding: document ใหม่ต่อการ resolve หนึ่งครั้ง และ font registry ที่ถูกล็อก หน้า production-usage ต้นทางที่ลิงก์ไว้ในส่วน ดูเพิ่มเติม จัดทำเอกสารการรับประกันเหล่านั้นพร้อมการอ้างอิง PSR ของหน้าเหล่านั้น หน้า cookbook นี้กล่าวถึงการใช้งานซ้ำ และเลื่อนการอ้างอิงไปยังหน้าเหล่านั้น
ดูเพิ่มเติม
หัวข้อที่มีชื่อว่า “ดูเพิ่มเติม”- ส่งคืน PDF ที่สร้างขึ้นจาก controller — คู่มือแบบทำงานพร้อมกัน
- การใช้งานจริงของ Laravel —
GeneratePdfJobcallback และตารางการปรับแต่ง queue - การใช้งานจริงของ Symfony — ความปลอดภัยของ Messenger worker และ builder locator
- การใช้งานจริงของ CodeIgniter —
GeneratePdfJobjobHandlersและการจำกัดเส้นทาง