Production usage of the NextPDF Laravel package
At a glance
Section titled “At a glance”In production, resolve the document contract with constructor injection.
Handle PDF write failures with a specific exception. Move heavy or batch
generation to GeneratePdfJob, and wire explicit success and failure
callbacks.
Install
Section titled “Install”composer require nextpdf/laravelphp artisan vendor:publish --tag=nextpdf-configConfigure the queue connection in config/nextpdf.php. Set
queue.connection, queue.queue, and queue.timeout. Then make sure a
worker runs on the configured connection.
Conceptual overview
Section titled “Conceptual overview”The container exposes NextPDF\Contracts\PdfDocumentInterface as a
factory binding. Each resolution yields a fresh
NextPDF\Core\Document. PSR-11 allows a container to return different
values from successive get() calls, depending on the binding strategy
(PSR-11 §1.1.2). This package uses a factory binding so request-scoped
mutable state never crosses requests. The font and image registries are
singletons. That preserves the contract that a bound identifier resolves
to its registered entry (PSR-11 §1.1.2), while still sharing the
expensive resources across the worker.
Prefer constructor injection over the facade in production code. It makes the dependency explicit, and it keeps the controller unit-testable without booting the facade root.
Code sample — Production
Section titled “Code sample — Production”DI-wired controller with typed error handling
Section titled “DI-wired controller with typed error handling”<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Response;use NextPDF\Contracts\PdfDocumentInterface;use NextPDF\Laravel\Http\PdfResponse;use Psr\Log\LoggerInterface;use Throwable;
final class InvoiceController extends Controller{ public function __construct( private readonly PdfDocumentInterface $document, private readonly LoggerInterface $logger, ) {}
public function show(int $invoiceId): Response { try { $this->document->addPage(); $this->document->cell(0, 10, "Invoice #{$invoiceId}", newLine: true); $this->document->cell(0, 10, 'Thank you for your business.');
return PdfResponse::download( $this->document, "invoice-{$invoiceId}.pdf", ); } catch (Throwable $exception) { // Rethrow as an HTTP-meaningful failure; never swallow. $this->logger->error('Invoice PDF generation failed', [ 'invoice_id' => $invoiceId, 'exception' => $exception::class, ]);
return new Response('Could not generate the invoice PDF.', 500); } }}Inject PdfDocumentInterface, not the concrete Document, so you can
swap the binding in tests. The container returns a fresh document for
each controller instantiation. Do not reuse the same controller instance
for two unrelated documents within one process.
The catch block logs the exception class and returns a defined HTTP
error instead of leaking a stack trace. Use
Psr\Log\LoggerInterface, which the container resolves to the framework
logger. PSR-3 leaves placeholder escaping to the implementor and tells
callers not to pre-escape context values (PSR-3 §1.2). Pass structured
context, not interpolated strings.
Queued generation with success and failure callbacks
Section titled “Queued generation with success and failure callbacks”GeneratePdfJob is a ShouldQueue job. It defaults to three tries, a
120-second timeout, and a 10-second backoff. You can override all three
in config/nextpdf.php. The builder closure receives the
container-resolved document and must return a configured document.
<?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 { // Dispatchable::dispatch() is `public static`: it constructs the // job from the arguments it receives and returns a PendingDispatch. // Pass every constructor argument — including the callbacks — to // the static call. Building an instance and then calling // `$job->dispatch(...)` would discard that instance (and its // callbacks) and queue a different job from only the static args. 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, ]); }, ); }}GeneratePdfJob::dispatch() forwards its arguments directly to the
constructor (string $outputPath, callable $builder, ?callable $onSuccess, ?callable $onFailure). As a result, the success and failure callbacks
are wired into the same job that gets queued. This matches the positional
GeneratePdfJob::dispatch($path, $builder) form in
/integrations/laravel/quickstart/. The success callback receives the
output path, and the failure callback receives the Throwable. The job
also exposes fluent then() and catch() setters that return the job for
chaining. Use those setters only when you keep and dispatch that same
instance, for example via the dispatch() helper. The job also exposes a
failed() method, which the queue runner invokes on terminal failure.
Callbacks are wrapped in serializable closures so they survive the queue
transport.
Tuning queue behavior
Section titled “Tuning queue behavior”| Property | Default | Config key |
|---|---|---|
tries | 3 | not config-driven; subclass to change |
timeout | 120 | nextpdf.queue.timeout |
backoff | 10 | not config-driven; subclass to change |
| queue name | pdf | nextpdf.queue.queue |
| connection | default | nextpdf.queue.connection |
tries and backoff are public properties read from the job instance.
The shipped job does not read them from config. If your retry policy
differs, subclass GeneratePdfJob to override them.
Edge cases & gotchas
Section titled “Edge cases & gotchas”- The builder closure must return a
PdfDocumentInterface. The job saves that return value, not the originally resolved instance. The job test asserts this contract explicitly. - Resolving
SignerInterfacereturnsnullunless signing is enabled and a certificate is configured andnextpdf/premiumis installed. Always null-check before signing. - Long-lived workers (Octane/RoadRunner/Swoole) share the locked font
registry. Configure
preload_fontsso warmup runs once at worker boot instead of on the first request. - A failed job invokes
failed()after exhaustingtries. The per-attempt failure does not callonFailureuntil the queue runner declares terminal failure.
Performance
Section titled “Performance”Synchronous controller generation blocks the request for the full PDF
build. For multi-page or batch output, dispatch GeneratePdfJob and
return immediately. The singleton registries spread font parsing and
image decoding across the worker lifetime. Per-request cost is then
limited to document construction and content emission.
Security notes
Section titled “Security notes”The dependency-injection controller logs the exception class, not its
message or trace, to avoid leaking internal detail into logs.
GeneratePdfJob validates the output path on the worker to mitigate
tampered serialized payloads on the queue transport. Full coverage is in
/integrations/laravel/security-and-operations/.
Conformance
Section titled “Conformance”| Claim | Source | Clause | reference_id |
|---|---|---|---|
| Bound identifier resolves to its registered entry | PSR-11 Container | §1.1.2 | |
| Successive resolutions may differ by binding strategy (factory binding) | PSR-11 Container | §1.1.2 |
PSR-3 logging guidance appears in the PSR-3 specification. That guidance
assigns placeholder escaping to the implementor and directs callers to
pass structured context. See doc psr_3_logger §1.2.
Commercial context
Section titled “Commercial context”Signed PAdES B-B output and PDF/A archival via nextpdf/premium use the same dependency injection (DI) surface. This is an optional Enterprise capability. The Core package documented here needs no code change to adopt it. See https://nextpdf.dev/get-license/?intent=laravel-signing.
See also
Section titled “See also”- /integrations/laravel/quickstart/ — minimal first example
- /integrations/laravel/configuration/ — queue, signature, and font keys
- /integrations/laravel/security-and-operations/ — threat model and hardening
- /integrations/laravel/troubleshooting/ — common production failures