Aller au contenu

Générer un PDF dans un job de file d'attente

La génération lourde de PDF n’a pas sa place dans le thread de requête. Chaque intégration de framework fournit une surface de génération en file d’attente qui construit et enregistre un PDF sur un worker, si bien que la requête HTTP répond dès que le job est mis en file. Ce guide couvre le flux en file d’attente pour Laravel (GeneratePdfJob), Symfony (GeneratePdfMessage via Messenger) et CodeIgniter 4 (GeneratePdfJob via codeigniter4/queue).

Les prérequis sont les suivants :

  • Le cœur NextPDF et une intégration de framework sont installés.
  • Un transport de worker est configuré : une connexion de file d’attente Laravel, un transport Symfony Messenger ou une file d’attente CodeIgniter 4 avec codeigniter4/queue installé.
  • Un processus worker fonctionne pour ce transport.

Ce guide suppose une application qui dispose déjà d’une file d’attente. Pour configurer la file d’attente ou Messenger lui-même, consulte la documentation de ton framework.

Installe l’intégration, puis la dépendance de file d’attente requise par ton framework.

Fenêtre de terminal
composer require nextpdf/laravel
Fenêtre de terminal
composer require nextpdf/symfony symfony/messenger

CodeIgniter nécessite le package de file d’attente. L’intégration le déclare uniquement comme dépendance de développement ; ajoute-le donc directement à l’application qui exécute les workers.

Fenêtre de terminal
composer require nextpdf/codeigniter codeigniter4/queue

Pour Laravel, configure la connexion de file d’attente dans config/nextpdf.php (queue.connection, queue.queue, queue.timeout) et lance un worker pour cette connexion.

Chaque intégration exprime la même idée selon ses propres conventions :

  • Laravel fournit NextPDF\Laravel\Jobs\GeneratePdfJob, un job ShouldQueue. Tu le dispatches avec un chemin de sortie et une closure de builder. La closure reçoit un document résolu par le conteneur et renvoie le document configuré. Sur le worker, le job écrit ce document renvoyé vers le chemin. Il accepte aussi des callbacks de succès et d’échec optionnels.
  • Symfony fournit NextPDF\Symfony\Message\GeneratePdfMessage, un message readonly dispatché sur le bus Messenger, accompagné de GeneratePdfHandler, qui résout un builder par son nom de classe depuis un service locator PSR-11. Tu implémentes NextPDF\Symfony\Message\PdfBuilderInterface pour chaque type de document.
  • CodeIgniter 4 fournit NextPDF\CodeIgniter\Jobs\GeneratePdfJob, enregistré sous une clé nommée dans Config\Queue::$jobHandlers. Tu pousses le job avec son nom enregistré, une référence de builder, un chemin de sortie et un tableau de contexte. Le builder est une méthode statique confinée à l’espace de noms App\PdfBuilders.

Les trois partagent la même approche de sécurité : le chemin de sortie est validé. Symfony et CodeIgniter le revalident au moment de la consommation, car un payload peut rester dans une file d’attente entre le dispatch et l’exécution. Le builder s’exécute sur un document neuf sur le worker, si bien que des jobs concurrents ne partagent jamais l’état d’un document.

PréoccupationLaravelSymfonyCodeIgniter 4
Unité en file d’attenteGeneratePdfJob (ShouldQueue)GeneratePdfMessage (DTO) + GeneratePdfHandlerGeneratePdfJob (handler de file d’attente)
DispatchGeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure)MessageBusInterface::dispatch(new GeneratePdfMessage(...))service('queue')->push($queue, $name, $data)
Forme du buildercallable(PdfDocumentInterface): PdfDocumentInterfacePdfBuilderInterface::build(Document, array): Documentstatic fn(Document, array): Document sous App\PdfBuilders
Garde-fou de chemin / d’entréeLe job valide le chemin de sortie sur le workerLe DTO valide à la construction, le handler revalide à la consommationLe job confine le chemin à WRITEPATH/pdfs/ et place l’espace de noms du builder sur liste d’autorisation
Surface d’échecfailed() après tries ; onFailure en cas d’échec terminalStratégie de retry Messenger ; erreurs de validation typéesInvalidArgumentException / QueueException

Le dispatch minimal pour chaque framework.

Laravel: dispatch GeneratePdfJob
<?php
declare(strict_types=1);
use NextPDF\Contracts\PdfDocumentInterface;
use NextPDF\Laravel\Jobs\GeneratePdfJob;
GeneratePdfJob::dispatch(
storage_path('app/reports/january-2026.pdf'),
static fn (PdfDocumentInterface $document): PdfDocumentInterface => $document
->addPage()
->cell(0, 10, 'January report', newLine: true),
);

Le chemin de sortie doit se terminer par .pdf ; le job valide le chemin sur le worker avant d’écrire.

Symfony: dispatch GeneratePdfMessage from a controller
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Pdf\InvoicePdfBuilder;
use NextPDF\Symfony\Message\GeneratePdfMessage;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route;
final class ReportController
{
#[Route('/invoice/{id}/queue', name: 'invoice_queue')]
public function queue(MessageBusInterface $bus, int $id): Response
{
$bus->dispatch(new GeneratePdfMessage(
builderClass: InvoicePdfBuilder::class,
outputPath: '/var/storage/invoices/' . $id . '.pdf',
builderContext: ['invoice_id' => $id],
));
return new Response('PDF generation queued.', 202);
}
}
CodeIgniter 4: push GeneratePdfJob by its registered name
<?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]);
}
}

Avec CodeIgniter, tu pousses la clé jobHandlers ('generate-pdf'), pas la chaîne de la classe du job. Enregistre d’abord le handler dans app/Config/Queue.php.

CodeIgniter 4: 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,
];
}

Un dispatch de production associe les callbacks de succès et d’échec (Laravel), ou un builder explicitement enregistré et un handler typé (Symfony), puis journalise via un logger PSR-3. L’exemple Laravel ci-dessous dispatche avec les deux callbacks.

Laravel: app/Jobs/DispatchMonthlyStatement.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
{
// dispatch() is public static: it constructs the job from the
// arguments it receives. Pass every argument — including the
// callbacks — to the static call, not to a separately built instance.
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,
]);
},
);
}
}

Le callback de succès reçoit le chemin de sortie ; le callback d’échec reçoit le Throwable. Le job épuise tries (par défaut 3) avant d’exécuter le parcours d’échec. Ajuste timeout via nextpdf.queue.timeout. Les valeurs tries et backoff sont des propriétés publiques ; crée donc une sous-classe de GeneratePdfJob pour les modifier.

Pour Symfony, implémente le builder et enregistre-le dans un service locator afin que seuls les builders enregistrés soient accessibles depuis le handler.

Symfony: src/Pdf/InvoicePdfBuilder.php
<?php
declare(strict_types=1);
namespace App\Pdf;
use NextPDF\Core\Document;
use NextPDF\Symfony\Message\PdfBuilderInterface;
final class InvoicePdfBuilder implements PdfBuilderInterface
{
/** @param array<string, mixed> $context */
public function build(Document $document, array $context): Document
{
$document->addPage();
$document->setFont('dejavusans', '', 12);
$document->cell(0, 10, 'Invoice #' . $context['invoice_id']);
return $document;
}
}
Symfony: config/services.yaml (builder locator)
services:
App\Pdf\InvoicePdfBuilder: ~
nextpdf.pdf_builder_locator:
class: Symfony\Component\DependencyInjection\ServiceLocator
arguments:
- 'App\Pdf\InvoicePdfBuilder': '@App\Pdf\InvoicePdfBuilder'
tags: ['container.service_locator']
NextPDF\Symfony\Message\GeneratePdfHandler:
arguments:
$builderLocator: '@nextpdf.pdf_builder_locator'

Pour CodeIgniter, implémente le builder comme une méthode statique sous App\PdfBuilders. Le job rejette toute référence de builder hors de cet espace de noms, ainsi que tout chemin de sortie hors de WRITEPATH/pdfs/.

CodeIgniter 4: app/PdfBuilders/InvoiceBuilder.php
<?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;
}
}

Lance le worker pour chaque framework.

Fenêtre de terminal
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600
Fenêtre de terminal
php spark queue:work pdf-queue

Recycle les workers Laravel et Symfony avec des durées de vie bornées (--limit / --memory-limit / --time-limit) pour qu’une allocation qui fuit dans une dépendance ne puisse pas croître sans limite.

  • La valeur de retour du builder est celle qui est enregistrée. Dans chaque intégration, le worker enregistre le document que le builder renvoie, pas l’instance résolue à l’origine. Renvoie toujours le document configuré depuis le builder.
  • La validation du chemin s’exécute sur le worker. Symfony valide le chemin de sortie à la construction, puis de nouveau au moment de la consommation. CodeIgniter confine le chemin à WRITEPATH/pdfs/ et rejette les chemins de traversée et de préfixe frère. Un chemin qui était sûr au dispatch, mais qui ne l’est plus à la consommation, est tout de même rejeté.
  • CodeIgniter pousse le nom, pas la classe. Pousser GeneratePdfJob::class comme nom de job est rejeté par la file d’attente au moment du push. Pousse plutôt la clé jobHandlers.
  • Les callbacks Laravel doivent être passés au dispatch statique. Construire une instance de job puis appeler $job->dispatch(...) écarte cette instance et ses callbacks. Passe les callbacks à GeneratePdfJob::dispatch(...).
  • Registres sûrs côté worker. Le registre de polices est un singleton verrouillé pendant toute la durée de vie du processus, et le registre d’images est un cache borné. Les documents sont neufs à chaque job. Ne demande pas de document partagé sur le worker.
  • Signature dans les workers. Une sortie signée ou PDF/A dans un job de file d’attente exige une édition commerciale NextPDF installée dans l’environnement du worker ; sans elle, le service de signature est résolu en null. Vérifie qu’il n’est pas null avant de signer.

Déplacer la génération vers un job en file d’attente retire l’intégralité du temps de construction du PDF de la requête HTTP : la requête répond une fois le travail mis en file. Les registres de polices et d’images amortissent leur coût de mise en place sur la durée de vie du worker, si bien que le coût par job se limite à la construction du document et à l’émission du contenu. Dimensionne le nombre de jobs en vol selon ton pool de workers, et pré-remplis preload_fonts (Laravel, Symfony) pour que le préchauffage des polices ait lieu une seule fois au démarrage du worker plutôt qu’au premier job.

  • Les payloads de file d’attente sont influençables par un attaquant dès que le broker est joignable ; traite donc le chemin de sortie et la référence de builder d’un payload comme non fiables. Les intégrations l’imposent par la validation du chemin et, sous CodeIgniter, par une liste d’autorisation de l’espace de noms du builder.
  • Restreins les permissions du système de fichiers du worker au répertoire de sortie prévu, en défense en profondeur, pour qu’un chemin altéré qui passerait malgré tout la validation ne puisse pas s’échapper du répertoire.
  • Journalise la classe de l’exception et un identifiant de corrélation dans le callback d’échec, jamais le message ni la trace.
  • N’écris jamais un bloc catch vide. Chaque callback d’échec ici journalise et transmet du contexte.

Le modèle de menace complet de la file d’attente — validation du payload, listes d’autorisation de callables et confinement du chemin — est détaillé dans la page sécurité-et-opérations de chaque intégration.

Ce guide ne fait aucune revendication normative de conformité aux standards. Chaque appel d’API présenté appartient à la surface publique vérifiée de l’intégration nommée. Les garanties de liaison au conteneur sur lesquelles repose le flux en file d’attente (un document neuf par résolution, le registre de polices verrouillé) sont documentées avec leurs citations PSR dans les pages d’usage en production liées sous Voir aussi. Cette page du Cookbook reformule l’usage et renvoie aux citations de ces pages.