Aller au contenu

NextPDF Symfony en production

Le bundle cible les runtimes PHP à exécution longue. Les documents ne sont pas partagés, le registre de polices est verrouillé après le warmup et le cache d’images est réinitialisé entre les requêtes. Sers les gros PDF en streaming et délègue les tâches lourdes aux workers Messenger.

Cycle de vie des services compatible avec les workers

Section intitulée « Cycle de vie des services compatible avec les workers »

Les runtimes à exécution longue gardent le conteneur en vie d’une requête à l’autre ; l’état propre à chaque requête ne doit donc pas fuiter. FrankenPHP, RoadRunner et les workers Messenger suivent tous ce modèle. Le services.php du bundle encode le cycle de vie ci-dessous, vérifié à partir des définitions de services :

  • Document — non partagé. nextpdf.document (et les alias PdfDocumentInterface / Document) est résolu en une nouvelle instance à chaque appel. Sous PSR-11, un conteneur peut légitimement renvoyer une valeur différente à chaque get() pour le même id (PSR-11 §1.1.2). Résous un document par requête. N’en conserve jamais un d’une requête à l’autre.
  • FontRegistry — partagé et verrouillé. Le registre est un singleton qui vit pendant toute la durée du processus. Après warmup() (lorsque preload_fonts n’est pas vide), la passe de compilation appelle lock(). Ce verrou empêche toute mutation à l’exécution et donc toute pollution de l’état des polices entre requêtes.
  • ImageRegistry — partagé, réinitialisé par requête. Le cache d’images, borné et de type least-recently-used (LRU), est partagé, mais marqué kernel.reset avec la méthode reset, si bien que Symfony le vide entre les requêtes sous les runtimes qui respectent kernel.reset.
  • Contrats EInvoice — non partagés. Lorsque les implémentations Premium sont présentes, les services embedder, validator, profile et schematron sont enregistrés comme non partagés. Le contexte du parseur, propre à chaque appel, ne fuit jamais d’une requête à l’autre.

Injecte PdfFactory — un composant partagé et sans état qui porte la configuration — puis appelle create() par requête :

public function __construct(private readonly PdfFactory $pdf) {}
public function action(): Response
{
$doc = $this->pdf->create(); // fresh, disposable
// ... build ...
return PdfResponse::inline($doc, 'document.pdf');
}

N’injecte pas Document ni nextpdf.document dans un service lui-même partagé et conservé d’une requête à l’autre. Résous-le plutôt à l’intérieur de la méthode, dans le périmètre de la requête.

PdfResponse::streamDownload() et streamInline() renvoient un StreamedResponse. Son callback émet le corps du PDF par blocs de 64 Ko et vide le tampon après chaque bloc. La taille du tampon de réponse reste ainsi bornée pour les gros documents. Les deux compromis ci-dessous sont vérifiés par rapport à PdfResponse :

  • Les variantes en streaming omettent intentionnellement Content-Length (l’objet de réponse ne connaît pas la taille du corps à l’avance). Les barres de progression de téléchargement et certains proxys préfèrent une longueur connue. Utilise les variantes sans streaming download() ou inline() lorsque le document est assez petit pour tenir en mémoire et qu’une longueur de contenu est souhaitable.
  • Les variantes en streaming émettent les mêmes en-têtes de sécurité et le même Cache-Control: private, max-age=0, must-revalidate que les variantes bufferisées.

Choisis le streaming pour les rapports de plusieurs mégaoctets et les exports par lots. Choisis les variantes bufferisées pour les réponses de petite taille et sensibles à la latence.

Délègue la génération à Messenger lorsque les requêtes doivent répondre rapidement, ou lorsque le rendu consomme beaucoup de CPU.

  1. Implémente PdfBuilderInterface pour chaque type de document.
  2. Enregistre les builders dans un container.service_locator et câble-le sur le GeneratePdfHandler en tant que $builderLocator.
  3. Route GeneratePdfMessage vers un transport durable.
  4. Exécute les workers avec des durées de vie bornées.

Recycle les workers afin qu’une allocation qui fuit dans une dépendance tierce ne puisse pas croître sans limite :

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

Les clés de configuration messenger.timeout et messenger.retries du bundle enregistrent le délai d’expiration par message et le budget de réessais cibles. Applique le comportement correspondant via la stratégie de réessai de Symfony et les flags du worker.

GeneratePdfMessage valide le chemin de sortie à la construction. Ensuite, GeneratePdfHandler le revalide au moment de l’exécution, avant d’écrire sur le disque. Cette vérification en deux étapes est importante pour le travail asynchrone. Un message peut rester en file d’attente entre l’envoi et la consommation ; le handler ne fait donc pas aveuglément confiance au chemin mis en file. Restreins les droits d’accès au système de fichiers du worker au répertoire de sortie prévu, en défense en profondeur.

Les services FontRegistry et ImageRegistry acceptent un Psr\Log\LoggerInterface facultatif (lié avec nullOnInvalid()). Lorsque l’application fournit un logger, les registres peuvent émettre des diagnostics par son intermédiaire. Le logger est un collaborateur facultatif et interchangeable selon le contrat de logger PSR-3 (PSR-3). Pour obtenir une visibilité à l’échelle de la requête, journalise autour de PdfFactory::create() et du handler Messenger dans le code de ton application. Utilise messenger:consume -vv lors du diagnostic d’incident.

  • Épingle une seule version majeure de nextpdf/core dans le composer.json de l’application (le bundle accepte ^3.0 || ^5.2).
  • Assure-toi que ext-mbstring et ext-zlib sont activées dans l’image PHP déployée (sinon le bundle échoue tôt au démarrage).
  • Pré-remplis preload_fonts pour les polices qu’utilisent tes documents, afin que le registre soit préchauffé et verrouillé au démarrage plutôt qu’à la première requête.
  • Pointe cache_path vers un emplacement persistant et accessible en écriture si tu t’appuies sur des artefacts mis en cache d’un déploiement à l’autre. Sinon, la valeur par défaut %kernel.cache_dir% convient.
  • Lance php bin/console cache:warmup pendant le déploiement afin que le conteneur compilé (y compris les sondes d’extensions optionnelles) soit construit avant le trafic.
  • Utilise un transport Messenger durable (pas sync) pour le travail asynchrone en production, et recycle les workers avec --limit / --memory-limit / --time-limit.
  • Réponses en streaming derrière un proxy à mise en tampon — un proxy qui bufferise tout le corps annule le gain de mémoire. Configure le proxy pour diffuser les réponses PDF en streaming, ou utilise des réponses bufferisées dans ce cas.
  • kernel.reset non honoré — sous un runtime qui n’appelle pas kernel.reset, le cache d’images est borné par image_cache_mb mais non vidé entre les requêtes ; dimensionne le plafond en conséquence.
  • Conserver un document d’une requête à l’autre — un Document capturé lors d’une requête précédente portera un état périmé. Résous-le toujours par requête via PdfFactory.

Chaque ligne correspond à une affirmation normative formulée sur cette page, rattachée à un reference_id complet de 64 caractères hexadécimaux, issu du corpus SDO sous accès contrôlé. La provenance, c’est-à-dire le manifeste du corpus et le transport de récupération, se trouve dans _sidecars/rag-citations.yaml.

SpecClausereference_idAffirmation
PSR-11psr_11_container#1.1.2.p3.bService non partagé : valeur distincte à chaque résolution
PSR-3psr_3_logger#x3.p17Collaborateur logger optionnel
  • /integrations/symfony/configuration/ — cycle de vie des services et paramètres.
  • /integrations/symfony/security-and-operations/ — en-têtes de réponse, validation de chemin, gestion des clés.
  • /integrations/symfony/troubleshooting/ — diagnostics de démarrage et d’exécution.
  • /integrations/symfony/quickstart/ — la configuration asynchrone minimale.