Production usage in CodeIgniter 4
At a glance
Section titled “At a glance”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.
Conceptual overview
Section titled “Conceptual overview”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).
API surface
Section titled “API surface”| Production concern | Verified surface |
|---|---|
| Resolve services | Services::pdf(false), Services::pdfDocument(false), Services::documentFactory() |
| Build response | PdfResponse::download() / inline() → DownloadResponse |
| Catch failures | NextPDF\Exception\NextPdfException (ecosystem base type) |
| Async generation | GeneratePdfJob registered in Config\Queue::$jobHandlers |
| Path / callable guards | GeneratePdfJob 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.
Worker-safe service lifetimes
Section titled “Worker-safe service lifetimes”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.
1. Register the job handler by name
Section titled “1. Register the job handler by name”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, ];}2. Dispatch by the registered name
Section titled “2. Dispatch by the registered name”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; }}Run the worker
Section titled “Run the worker”php spark queue:work pdf-queueEach 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.
Edge cases & gotchas
Section titled “Edge cases & gotchas”- The queue rejects
GeneratePdfJob::classas the job name at push time because it is not the registered key'generate-pdf'. Always push thejobHandlerskey. - The builder string must match
App\PdfBuilders\<Class>::<method>exactly. Functions, other namespaces, or prefixed or suffixed payloads raiseInvalidArgumentExceptionbefore 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/queueis a development-only dependency of the package. Require it in the application that runs workers.
Performance
Section titled “Performance”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.
Security notes
Section titled “Security notes”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.
Conformance
Section titled “Conformance”- Controllers receive concrete services, not a container, consistent with PSR-11 §1.3 service-locator guidance.
Commercial context
Section titled “Commercial context”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>.
See also
Section titled “See also”- /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.