Skip to content

Production usage of the NextPDF Laravel package

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.

Terminal window
composer require nextpdf/laravel
php artisan vendor:publish --tag=nextpdf-config

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

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.

DI-wired controller with typed error handling

Section titled “DI-wired controller with typed error handling”
resource: NextPDF\Contracts\PdfDocumentInterface + src/Laravel/Http/PdfResponse.php
<?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.

resource: src/Laravel/Jobs/GeneratePdfJob.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
{
// 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.

PropertyDefaultConfig key
tries3not config-driven; subclass to change
timeout120nextpdf.queue.timeout
backoff10not config-driven; subclass to change
queue namepdfnextpdf.queue.queue
connectiondefaultnextpdf.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.

  • 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 SignerInterface returns null unless signing is enabled and a certificate is configured and nextpdf/premium is installed. Always null-check before signing.
  • Long-lived workers (Octane/RoadRunner/Swoole) share the locked font registry. Configure preload_fonts so warmup runs once at worker boot instead of on the first request.
  • A failed job invokes failed() after exhausting tries. The per-attempt failure does not call onFailure until the queue runner declares terminal failure.

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.

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

ClaimSourceClausereference_id
Bound identifier resolves to its registered entryPSR-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.

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.

  • /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