Salta ai contenuti

Uso in produzione con CodeIgniter 4

I controller di produzione dipendono da servizi NextPDF concreti. Gestiscono esplicitamente la gerarchia di eccezioni documentata ed emettono segnali di osservabilità. La generazione PDF di lunga durata viene spostata fuori dalla richiesta tramite la coda di CodeIgniter 4.

CodeIgniter 4 risolve i servizi del pacchetto tramite il proprio locator. Il pattern service-locator passa un container a un oggetto affinché l’oggetto recuperi le proprie dipendenze. Questo pattern è sconsigliato (PSR-11 §1.3, verbo modale SHOULD NOT). Per seguire questa indicazione, risolvere ciascun servizio NextPDF una sola volta al confine del controller e passare l’oggetto concreto al codice interno. Non passare la classe Services — né un container — al codice di dominio.

Ogni esempio PHP dichiara declare(strict_types=1); su una riga dedicata (PSR-12 §x1.x3.p34).

Aspetto di produzioneSuperficie verificata
Risolvere i serviziServices::pdf(false), Services::pdfDocument(false), Services::documentFactory()
Costruire la rispostaPdfResponse::download() / inline()DownloadResponse
Intercettare gli erroriNextPDF\Exception\NextPdfException (tipo base dell’ecosistema)
Generazione asincronaGeneratePdfJob registrato in Config\Queue::$jobHandlers
Controlli su path / callableGeneratePdfJob lancia InvalidArgumentException

Controller di produzione — gestione degli errori e osservabilità

Sezione intitolata “Controller di produzione — gestione degli errori e osservabilità”

Le eccezioni lanciate dal motore core estendono tutte NextPDF\Exception\NextPdfException. Intercettando quel singolo tipo si coprono gli errori del core e delle estensioni. Qui il blocco catch registra l’errore con contesto e restituisce una risposta di errore definita, mai un catch vuoto.

<?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) restituisce una nuova libreria e un nuovo documento sottostante a ogni chiamata. Le richieste concorrenti quindi non condividono mai lo stato del documento. I test funzionali del pacchetto verificano questo comportamento.

I registri dei font e delle immagini sono, per progettazione, singleton con ciclo di vita pari a quello del processo. Il registro dei font viene preriscaldato e bloccato una sola volta. Il registro delle immagini è una cache limitata least-recently-used (LRU). In un worker di lunga durata (server spark di CodeIgniter, runner in stile RoadRunner o worker di coda) questo è il comportamento previsto: i registri costosi persistono, mentre ogni documento è nuovo. Non richiedere un documento condiviso (Services::pdfDocument(true)) nel codice della richiesta o del job; esiste solo per il reset dei test e condividerebbe il contenuto tra le richieste.

GeneratePdfJob esegue la generazione del PDF fuori dalla richiesta tramite codeigniter4/queue. Il runtime della coda impone due requisiti, che devono essere entrambi configurati correttamente.

La coda risolve un job tramite una chiave nominale, non tramite una stringa di classe. L’handler della coda valida il nome del job inviato confrontandolo con le chiavi di Config\Queue::$jobHandlers. Rifiuta un nome sconosciuto con CodeIgniter\Queue\Exceptions\QueueException. Registrare il 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,
];
}

Inviare il job usando il nome registrato come secondo argomento. Il primo argomento è il nome della coda. Il terzo argomento è l’array dei dati del job.

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

Il job limita i callable del builder al namespace App\PdfBuilders e confina i path di output in WRITEPATH/pdfs/. Il builder è un metodo statico. Riceve un nuovo Document e l’array di contesto, poi restituisce il documento.

<?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

Ogni esecuzione del job parte da un nuovo documento tramite Services::pdfDocument(). Applica il builder, quindi lo salva nel path convalidato. I test del pacchetto verificano che due esecuzioni consecutive del job non condividano lo stato del documento.

  • La coda rifiuta GeneratePdfJob::class come nome del job al momento dell’invio, perché non è la chiave registrata 'generate-pdf'. Inviare sempre la chiave registrata in jobHandlers.
  • La stringa del builder deve corrispondere esattamente a App\PdfBuilders\<Class>::<method>. Funzioni, altri namespace o payload con prefisso o suffisso sollevano InvalidArgumentException prima che venga eseguito qualsiasi codice.
  • Il path di output deve risolversi all’interno di WRITEPATH/pdfs/ e terminare con .pdf (senza distinzione tra maiuscole e minuscole). I path di traversal e quelli con prefisso di pari livello vengono rifiutati.
  • codeigniter4/queue è una dipendenza del pacchetto solo in fase di sviluppo. Richiederla nell’applicazione che esegue i worker.

I registri vengono creati una sola volta per ogni processo worker. Il costo di costruzione del documento dipende dal contenuto, non dall’adapter. Per i job batch di grandi dimensioni, preferire il percorso della coda affinché i worker delle richieste restino reattivi. Impostare un performance_budget in qualsiasi ricetta che abbia un obiettivo misurabile.

Il job di coda è la superficie a rischio più elevato. I payload della coda possono essere manipolati da un attaccante quando il broker è raggiungibile. L’allowlist dei callable e il confinamento dei path sono trattati in /integrations/codeigniter/security-and-operations/ con i casi di rifiuto verificati.

  • I controller ricevono servizi concreti, non un container, coerentemente con l’indicazione sul service-locator di PSR-11 §1.3.

Il core di NextPDF è Apache-2.0. L’output firmato e PDF/A nei job di coda richiede NextPDF Pro o Enterprise installato nell’ambiente del worker. Il pacchetto CodeIgniter espone i metodi di servizio corrispondenti. Questi metodi restituiscono null finché non è installato il pacchetto Premium corrispondente. Vedere </get-license/?intent=codeigniter-async-signing>.

  • /integrations/codeigniter/quickstart/ — la versione essenziale di questi controller.
  • /integrations/codeigniter/configuration/ — firma, TSA e configurazione dei path.
  • /integrations/codeigniter/security-and-operations/ — modello di minaccia della coda e hardening.
  • /integrations/codeigniter/troubleshooting/ — modalità di errore della coda e della discovery.
  • /integrations/codeigniter/integration/ — riferimento per il wiring e gli smoke test.