Lewati ke konten

Menghasilkan PDF dalam job yang diantrekan

Pembuatan PDF yang berat tidak boleh berjalan di thread permintaan. Setiap integrasi framework menyediakan API pembuatan yang diantrekan untuk membangun dan menyimpan PDF di worker. Permintaan HTTP dapat langsung merespons setelah Anda mengirim pekerjaan tersebut ke antrean. Panduan ini membahas jalur yang diantrekan untuk Laravel (GeneratePdfJob), Symfony (GeneratePdfMessage melalui Messenger), dan CodeIgniter 4 (GeneratePdfJob melalui codeigniter4/queue).

Prasyaratnya adalah:

  • NextPDF core dan satu integrasi framework sudah terpasang.
  • Transport worker sudah dikonfigurasi: koneksi antrean Laravel, transport Symfony Messenger, atau antrean CodeIgniter 4 dengan codeigniter4/queue terpasang.
  • Proses worker sedang berjalan untuk transport tersebut.

Panduan ini mengasumsikan aplikasi Anda sudah memiliki antrean. Untuk menyiapkan antrean atau Messenger, rujuk dokumentasi framework Anda.

Pasang integrasinya, lalu pasang dependensi antrean yang diperlukan framework Anda.

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

CodeIgniter memerlukan paket antrean. Integrasi ini mendeklarasikannya sebagai dependensi khusus pengembangan, jadi pasang dengan require pada aplikasi yang menjalankan worker.

Terminal window
composer require nextpdf/codeigniter codeigniter4/queue

Untuk Laravel, konfigurasikan koneksi antrean di config/nextpdf.php (queue.connection, queue.queue, queue.timeout), lalu jalankan worker untuk koneksi itu.

Setiap integrasi mengikuti pola yang sama, dengan gaya framework-nya masing-masing:

  • Laravel menyertakan NextPDF\Laravel\Jobs\GeneratePdfJob, sebuah job ShouldQueue. Anda mengirimnya ke antrean bersama jalur keluaran dan closure builder. Closure tersebut menerima dokumen yang di-resolve oleh container dan mengembalikan dokumen yang telah dikonfigurasi. Di worker, job menyimpan dokumen yang dikembalikan ke jalur tersebut. Job ini juga menerima callback keberhasilan dan kegagalan yang opsional.
  • Symfony menyertakan NextPDF\Symfony\Message\GeneratePdfMessage, sebuah pesan readonly yang dikirim ke bus Messenger, beserta GeneratePdfHandler. Handler ini me-resolve builder berdasarkan nama kelas dari service locator PSR-11. Anda mengimplementasikan NextPDF\Symfony\Message\PdfBuilderInterface untuk setiap jenis dokumen.
  • CodeIgniter 4 menyertakan NextPDF\CodeIgniter\Jobs\GeneratePdfJob, yang didaftarkan dengan sebuah kunci nama dalam Config\Queue::$jobHandlers. Anda mendorong job berdasarkan nama terdaftarnya, disertai referensi builder, jalur keluaran, dan sebuah array konteks. Builder adalah metode statis yang dibatasi ke namespace App\PdfBuilders.

Ketiga integrasi tersebut memiliki pendekatan keamanan yang sama: mereka memvalidasi jalur keluaran. Symfony dan CodeIgniter memvalidasinya lagi saat dikonsumsi, karena payload dapat berada di antrean antara pengiriman dan eksekusi. Builder berjalan pada dokumen baru di worker, sehingga job yang berjalan bersamaan tidak pernah berbagi state dokumen.

AspekLaravelSymfonyCodeIgniter 4
Unit yang diantrekanGeneratePdfJob (ShouldQueue)GeneratePdfMessage (DTO) + GeneratePdfHandlerGeneratePdfJob (handler antrean)
PengirimanGeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure)MessageBusInterface::dispatch(new GeneratePdfMessage(...))service('queue')->push($queue, $name, $data)
Bentuk buildercallable(PdfDocumentInterface): PdfDocumentInterfacePdfBuilderInterface::build(Document, array): Documentstatic fn(Document, array): Document di bawah App\PdfBuilders
Proteksi jalur / masukanJob memvalidasi jalur keluaran di workerDTO memvalidasi saat konstruksi; handler memvalidasi ulang saat konsumsiJob membatasi jalur ke WRITEPATH/pdfs/ dan hanya mengizinkan namespace builder lewat allowlist
Permukaan kegagalanfailed() setelah tries; onFailure pada kegagalan terminalStrategi percobaan ulang Messenger; kesalahan validasi bertipeInvalidArgumentException / QueueException

Gunakan pola pengiriman minimal ini di setiap 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),
);

Jalur keluaran harus diakhiri dengan .pdf; job memvalidasi jalur di worker sebelum menulis berkas.

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

Di CodeIgniter, dorong kunci jobHandlers ('generate-pdf'), bukan string kelas job. Daftarkan handler lebih dahulu di 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,
];
}

Pada pengiriman produksi, hubungkan callback keberhasilan dan kegagalan (Laravel), atau builder yang didaftarkan secara eksplisit beserta handler bertipe (Symfony), ke logger PSR-3. Contoh Laravel berikut melakukan dispatch dengan kedua callback tersebut.

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 keberhasilan menerima jalur keluaran. Callback kegagalan menerima Throwable. Job menghabiskan tries (standar 3) sebelum menjalankan jalur kegagalan. Atur timeout melalui nextpdf.queue.timeout. Nilai tries dan backoff adalah properti publik, jadi buat subkelas GeneratePdfJob untuk mengubahnya.

Untuk Symfony, implementasikan builder dan daftarkan di service locator. Ini menjaga handler tetap terbatas pada builder yang terdaftar.

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'

Untuk CodeIgniter, implementasikan builder sebagai metode statis di bawah App\PdfBuilders. Job menolak setiap referensi builder di luar namespace tersebut dan setiap jalur keluaran di luar 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;
}
}

Jalankan worker untuk setiap 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

Daur ulang worker Laravel dan Symfony dengan masa hidup terbatas (--limit / --memory-limit / --time-limit) agar alokasi yang bocor pada suatu dependensi tidak dapat tumbuh tanpa batas.

  • Yang disimpan adalah nilai kembalian builder. Pada setiap integrasi, worker menyimpan dokumen yang dikembalikan oleh builder, bukan instance yang awalnya di-resolve. Selalu kembalikan dokumen yang telah dikonfigurasi dari builder.
  • Validasi jalur dijalankan di worker. Symfony memvalidasi jalur keluaran saat konstruksi dan sekali lagi saat dikonsumsi. CodeIgniter membatasi jalur ke WRITEPATH/pdfs/ dan menolak jalur traversal serta jalur dengan prefiks saudara. Jalur yang aman saat pengiriman tetapi tidak aman saat dikonsumsi tetap ditolak.
  • CodeIgniter menggunakan nama, bukan kelasnya. Jika Anda mendorong GeneratePdfJob::class sebagai nama job, antrean menolaknya pada saat push. Sebagai gantinya, dorong kunci jobHandlers.
  • Callback Laravel harus diteruskan ke dispatch statis. Jika Anda membangun sebuah instance job lalu memanggil $job->dispatch(...), panggilan itu membuang instance beserta callback-nya. Teruskan callback ke GeneratePdfJob::dispatch(...).
  • Registri yang aman untuk worker. Registri fon adalah singleton terkunci dengan masa hidup proses, dan registri gambar adalah cache yang dibatasi. Dokumen selalu baru untuk setiap job. Jangan meminta dokumen bersama di worker.
  • Penandatanganan di worker. Keluaran yang ditandatangani atau PDF/A dalam sebuah job antrean memerlukan edisi komersial NextPDF yang terpasang di lingkungan worker. Tanpa edisi tersebut, layanan penandatanganan di-resolve menjadi null. Lakukan pemeriksaan null sebelum menandatangani.

Memindahkan proses pembuatan ke job yang diantrekan mengeluarkan seluruh waktu pembangunan PDF dari permintaan HTTP. Permintaan merespons begitu pekerjaan dikirim ke antrean. Registri fon dan gambar mengamortisasi biaya penyiapannya sepanjang masa hidup worker, sehingga biaya per job hanya terbatas pada konstruksi dokumen dan emisi konten. Sesuaikan jumlah job yang sedang berjalan dengan kumpulan worker Anda, dan isi preload_fonts di awal (Laravel, Symfony) agar pemanasan fon terjadi sekali saat worker booting, bukan pada job pertama.

  • Payload antrean dapat dipengaruhi penyerang jika broker dapat dijangkau, jadi perlakukan jalur keluaran dan referensi builder dalam payload sebagai tidak tepercaya. Integrasi menegakkannya dengan validasi jalur dan, di CodeIgniter, sebuah allowlist namespace builder.
  • Batasi izin sistem berkas worker hanya pada direktori keluaran yang dituju sebagai pertahanan berlapis. Jika sebuah jalur yang dimanipulasi entah bagaimana lolos validasi, jalur itu tetap tidak dapat keluar dari direktori.
  • Catat kelas exception dan sebuah pengenal korelasi dalam callback kegagalan; jangan pernah mencatat pesan atau jejaknya.
  • Jangan pernah menulis blok catch yang kosong. Setiap callback kegagalan di sini mencatat log dan menyertakan konteks.

Halaman keamanan dan operasi setiap integrasi membahas model ancaman antrean secara lengkap: validasi payload, allowlist callable, dan pembatasan jalur.

Panduan ini tidak membuat klaim standar normatif apa pun. Setiap panggilan API yang ditampilkan adalah permukaan publik terverifikasi dari integrasi yang disebutkan. Jalur yang diantrekan bergantung pada jaminan pengikatan container: dokumen baru untuk setiap resolusi dan registri fon yang terkunci. Halaman penggunaan produksi hulu yang ditautkan di bawah Lihat juga mendokumentasikan jaminan tersebut beserta kutipan PSR-nya. Halaman cookbook ini menyatakan ulang penggunaannya dan menyerahkan kutipan kepada halaman-halaman tersebut.