Skip to content

Production usage in CodeIgniter 4

Production controllers receive concrete NextPDF services. They handle the documented exception hierarchy explicitly and emit observability signals. Move long-running Portable Document Format (PDF) work off the request through the CodeIgniter 4 Queue.

CodeIgniter 4 resolves the package’s services through its locator. In a service-locator pattern, an object receives a container and uses it to retrieve its own dependencies. PHP Standard Recommendation (PSR) guidance discourages that pattern (PSR-11 §1.3, modal SHOULD NOT). To follow that guidance, resolve each NextPDF service once at the controller boundary, then pass the concrete object inward. Do not pass the Services class or a container into your domain code.

Each PHP sample puts declare(strict_types=1); on its own line (PSR-12 §x1.x3.p34).

Production concernVerified surface
Resolve servicesServices::pdf(false), Services::pdfDocument(false), Services::documentFactory()
Build responsePdfResponse::download() / inline()DownloadResponse
Catch failuresNextPDF\Exception\NextPdfException (ecosystem base type)
Async generationGeneratePdfJob registered in Config\Queue::$jobHandlers
Path / callable guardsGeneratePdfJob throws InvalidArgumentException

Production controller — error handling and observability

Section titled “Production controller — error handling and observability”

The core engine throws exceptions that all extend NextPDF\Exception\NextPdfException. Catch this single type to cover core and extension failures. This catch block logs context and returns a defined error response, never an empty catch.

<?php
declare(strict_types=1);
namespace App\Controllers;
use CodeIgniter\HTTP\DownloadResponse;
use CodeIgniter\HTTP\ResponseInterface;
use NextPDF\CodeIgniter\Config\Services;
use NextPDF\Exception\NextPdfException;
use Psr\Log\LoggerInterface;
final class InvoiceController extends BaseController
{
public function download(int $id): DownloadResponse|ResponseInterface
{
/** @var LoggerInterface $logger */
$logger = \service('logger');
$start = \hrtime(true);
try {
$pdf = Services::pdf(false);
$pdf->document()->addPage();
$pdf->document()->cell(0, 10, "Invoice #{$id}");
$response = $pdf->download("invoice-{$id}.pdf");
$logger->info('pdf.invoice.generated', [
'invoice_id' => $id,
'elapsed_ms' => (\hrtime(true) - $start) / 1_000_000,
]);
return $response;
} catch (NextPdfException $e) {
$logger->error('pdf.invoice.failed', [
'invoice_id' => $id,
'exception' => $e::class,
'message' => $e->getMessage(),
]);
return $this->response
->setStatusCode(ResponseInterface::HTTP_INTERNAL_SERVER_ERROR)
->setJSON(['error' => 'pdf_generation_failed', 'invoice_id' => $id]);
}
}
}

Services::pdf(false) returns a fresh library and a fresh underlying document on every call. Concurrent requests never share document state. The package functional tests assert this behavior.

The font and image registries are process-lifetime singletons by design. The font registry warms and locks once. The image registry is a bounded least recently used (LRU) cache. In a long-lived worker (CodeIgniter spark server, RoadRunner-style runner, or a queue worker), this is intentional: expensive registries persist, while every document is fresh. Do not request a shared document (Services::pdfDocument(true)) in request or job code; it exists only for test reset and would share content across requests.

Asynchronous generation with CodeIgniter Queue

Section titled “Asynchronous generation with CodeIgniter Queue”

GeneratePdfJob runs PDF generation off the request through codeigniter4/queue. The queue runtime requires two settings. Configure both correctly.

The queue resolves a job by a name key, not by a class string. The queue handler validates the pushed job name against the keys of Config\Queue::$jobHandlers. It rejects an unknown name with CodeIgniter\Queue\Exceptions\QueueException. Register the job in 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,
];
}

Push the job with the registered name as the second argument. The first argument is the queue name. The third argument is the job data array.

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

3. Implement the builder under App\PdfBuilders

Section titled “3. Implement the builder under App\PdfBuilders”

The job allows builder callables only in the App\PdfBuilders namespace and confines output paths to WRITEPATH/pdfs/. Implement the builder as a static method. It receives a fresh Document and the context array, then returns the document.

<?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;
}
}
Terminal window
php spark queue:work pdf-queue

Each job run starts with a fresh document from Services::pdfDocument(). It applies the builder, then saves to the validated path. The package tests verify that two consecutive job runs do not share document state.

  • The queue rejects GeneratePdfJob::class as the job name at push time because it is not the registered key 'generate-pdf'. Always push the jobHandlers key.
  • The builder string must match App\PdfBuilders\<Class>::<method> exactly. Functions, other namespaces, or prefixed or suffixed payloads raise InvalidArgumentException before any code runs.
  • The output path must resolve inside WRITEPATH/pdfs/ and end in .pdf (case-insensitive). Traversal and sibling-prefix paths are rejected.
  • codeigniter4/queue is a development-only dependency of the package. Require it in the application that runs workers.

The registries are created once per worker process. Document build cost scales with content, not with the adapter. For large batch jobs, use the queue path so request workers stay responsive. Set a performance_budget in any recipe with a measurable target.

The queue job is the highest-risk surface. When the broker is reachable, queue payloads are attacker-influenced. The callable allowlist and path confinement are covered in /integrations/codeigniter/security-and-operations/ with the verified rejection cases.

  • Controllers receive concrete services, not a container, consistent with PSR-11 §1.3 service-locator guidance.

NextPDF core is Apache-2.0. To produce signed and PDF/A output in queue jobs, install NextPDF Pro or Enterprise in the worker environment. The CodeIgniter package exposes the corresponding service methods. They return null until the matching Premium package is installed. See </get-license/?intent=codeigniter-async-signing>.

  • /integrations/codeigniter/quickstart/ — the minimal version of these controllers.
  • /integrations/codeigniter/configuration/ — signing, Time Stamping Authority (TSA), and path configuration.
  • /integrations/codeigniter/security-and-operations/ — queue threat model and hardening.
  • /integrations/codeigniter/troubleshooting/ — queue and discovery failure modes.
  • /integrations/codeigniter/integration/ — wiring reference and smoke test.