Salta ai contenuti
getnextpdf.com

Eseguire NextPDF su piattaforme serverless

Il motore core nativo, in-process, di NextPDF è un workload serverless quasi ideale. È PHP puro che gira all’interno del proprio processocomposer require nextpdf/core, si costruisce un documento, si ottengono i byte. Non c’è alcun binario esterno da avviare, nessun browser headless, nessun daemon da tenere in vita e nessun socket verso un servizio sidecar. Una funzione che costruisce un PDF parte a freddo, esegue il proprio PHP, restituisce i byte ed esce. Questo si mappa in modo pulito su AWS Lambda (tramite il runtime Bref), Google Cloud Run e AWS App Runner.

Questa pagina copre la distribuzione di quel motore nativo su quei tre runtime e il piccolo insieme di vincoli reali che impongono:

  • il filesystem del runtime non è durevole: Lambda garantisce solo una /tmp scrivibile, mentre i runtime a container (Cloud Run, App Runner) hanno un filesystem effimero, con ambito al container — in entrambi i casi i font devono viaggiare all’interno del pacchetto di distribuzione o dell’immagine ed essere registrati in PHP (il motore non legge alcuna variabile d’ambiente di percorso dei font);
  • gli avvii a freddo pagano l’autoloading e qualsiasi warmup dei font, quindi riscaldare il FontRegistry una volta per container, non per invocazione;
  • dimensione del pacchetto, memoria e timeout devono essere dimensionati sulla build, non su una richiesta banale.

Questa pagina è solo per il motore nativo. Il bridge Chrome (writeHtmlChrome tramite il pacchetto suggerito nextpdf/artisan) è una storia diversa e più pesante: invoca un Chromium headless tramite symfony/process, che uno zip Lambda di base o un container snello non contiene. Eseguire Chromium su Lambda significa un layer personalizzato con il browser e le sue librerie condivise, pacchetti molto più grandi e avvii a freddo molto più lunghi — fuori ambito qui. Il motore nudo non ha bisogno di nulla di tutto ciò.

Prima di iniziare, confermare che questi elementi siano a posto:

  • La propria applicazione ha un composer.json e un composer.lock committati, con nextpdf/core come dipendenza.
  • Si dispone dei file di font che si intende incorporare e si è autorizzati a incorporarli.
  • Si dispone della toolchain per il proprio target — la CLI Bref e il framework serverless per Lambda, o una build a container per Cloud Run / App Runner.

Letto direttamente dal pacchetto, nextpdf/core richiede php: >=8.4 <9.0 e un piccolo insieme di estensioni PHP — ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib e ext-curl. I layer PHP standard di Bref includono ognuna di esse. Le immagini container ufficiali php:8.4 forniscono openssl, curl e zlib di serie, ma mbstring, gd e intl non sono incluse — richiedono l’installazione delle dipendenze di sistema e l’abilitazione delle estensioni con docker-php-ext-install (vedere la guida alla distribuzione con Docker). Su Bref non c’è nulla di esotico da compilare; sul percorso a container si abilitano quelle tre estensioni nella build dell’immagine per il motore nudo.

Ciò che rende pulita l’idoneità è ciò che il motore non fa:

  • Nessun sottoprocesso per il percorso core. Costruire un documento e chiamare getPdfData() è PHP in-process da capo a fine. La dipendenza symfony/process esiste per il bridge Chrome opzionale, non per il rendering nativo — la generazione PDF nativa non avvia mai un processo.
  • Nessuno stato persistente. Ogni invocazione costruisce un documento nuovo e restituisce byte. Nulla deve sopravvivere tra una richiesta e l’altra tranne il container caldo, che si sfrutta per il warmup dei font (sotto) ma su cui non si fa mai affidamento per la correttezza.
  • Nessuna directory di lavoro scrivibile necessaria. Il motore costruisce il PDF in memoria e lo restituisce come stringa; tocca il disco solo se si chiama save(). Su serverless non lo si fa — si restituiscono i byte — quindi l’assenza di un filesystem durevole non morde mai il percorso di build.

L’unico vincolo rigido: nessun filesystem scrivibile durevole

Sezione intitolata “L’unico vincolo rigido: nessun filesystem scrivibile durevole”

Il filesystem di distribuzione non è durevole, ma il modello differisce per runtime. AWS Lambda garantisce solo una /tmp scrivibile (512 MB per impostazione predefinita, configurabile fino a 10 GB); il resto del filesystem della funzione è in sola lettura. I runtime a container (Cloud Run, App Runner) hanno un filesystem scrivibile effimero, con ambito al container anziché un modello solo-/tmp — ma tutto ciò che vi viene scritto va perso quando il container viene riciclato, quindi è spazio scratch, non storage. In ogni caso, preferire /tmp o un volume configurato per lo staging, e non fare mai affidamento sulle scritture nel percorso dell’immagine dell’applicazione come storage durevole. Ne conseguono due implicazioni.

Non chiamare mai save() aspettandosi un output durevole. NextPDF\Core\Document espone sia save(string $path): void sia getPdfData(): string. Su serverless si usa getPdfData() e si restituiscono o caricano i byte — non trattare una scrittura nella directory dell’applicazione come storage persistente. Se si deve mettere in staging un file (per esempio, per fare l’upload multipart verso l’object storage), scrivere sotto /tmp (o un volume configurato) e ripulire, ricordando che su un container caldo questo spazio scratch persiste tra le invocazioni e conta verso il suo limite di dimensione.

use NextPDF\Core\Document;
// Right for serverless: get the bytes, return or upload them.
$pdf = $document->getPdfData(); // string of PDF bytes, built in memory
// Avoid on serverless: save() writes to disk. On Lambda the application
// directory is read-only; on Cloud Run / App Runner it is writable but
// ephemeral (lost on container recycle). Neither is durable storage.
// $document->save('/var/task/out.pdf'); // not durable — return the bytes instead

Non installare font del sistema operativo a runtime, e non fare affidamento sul discovery automatico dei font; includere i propri file di font per la produzione. Su Lambda il filesystem in sola lettura blocca del tutto apt-get install fonts-*; su un runtime a container qualsiasi installazione a runtime finisce su un filesystem effimero e va persa al riciclo successivo. E comunque non aiuterebbe, perché il motore nativo non legge font OS/fontconfig — risolve i font solo dai file che si registrano. Quindi per la produzione i file di font devono viaggiare all’interno dell’artefatto di distribuzione. Se si recuperano deliberatamente file di font in /tmp o in un volume configurato, si devono registrare esplicitamente con il font registry e accettare il costo aggiuntivo di avvio a freddo e di affidabilità — non è un pattern di produzione consigliato.

Includere e registrare i font nel pacchetto o nell’immagine

Sezione intitolata “Includere e registrare i font nel pacchetto o nell’immagine”

Il motore nativo risolve i font dai file di font tramite il NextPDF\Typography\FontRegistry, non da fontconfig o dai font installati nel sistema operativo. Su serverless questo non è negoziabile: non c’è alcun filesystem persistente su cui mettere i font dopo il deploy, quindi viaggiano all’interno del pacchetto (uno zip o un layer Lambda) o all’interno dell’immagine (Cloud Run / App Runner).

Includere i propri file .ttf / .otf / .ttc sotto una directory nel proprio progetto — resources/fonts/ è la convenzione — così da includerli nell’artefatto. Poi registrare quella directory in PHP. Il motore non legge alcuna variabile d’ambiente di percorso dei font: NEXTPDF_FONTS_PATH è il valore predefinito della chiave di configurazione fonts_path del pacchetto nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))) ed è consumata solo da quell’integrazione del framework, non da nextpdf/core. Una funzione nuda deve costruire il registry con la directory inclusa:

use NextPDF\Typography\FontRegistry;
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
// Register the directory the deployment artifact bundled the fonts into.
// On Lambda/Bref the code root is /var/task; adjust for your runtime.
$registry = new FontRegistry(__DIR__ . '/resources/fonts');
// (equivalently, $registry->addFontDirectory(__DIR__ . '/resources/fonts');)
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
$document = $factory->create();

Questa è l’intera questione serverless per i font. Le regole di denominazione dei file, l’API completa del registry e la gestione del filesystem non durevole vivono sulla pagina dedicata — non duplicarle qui. Leggere Predisporre i font per il motore nativo in produzione per il pattern completo, e registrare la stessa directory inclusa. La guida alla distribuzione con Docker copre l’inclusione equivalente lato immagine per il caso Cloud Run / App Runner.

Avvii a freddo: riscaldare il FontRegistry una volta per container

Sezione intitolata “Avvii a freddo: riscaldare il FontRegistry una volta per container”

Un avvio a freddo paga il bootstrap di PHP, l’autoloader ottimizzato di Composer e qualsiasi parsing dei font innescato dalla prima build. Non si può evitare il bootstrap, ma si può spostare il lavoro sui font fuori dal percorso caldo e riutilizzarlo tra le invocazioni a caldo.

Costruire il FontRegistry e il DocumentFactory una volta, fuori dall’handler, così da farli vivere per la durata del container e riutilizzarli a ogni invocazione a caldo. Facoltativamente chiamare warmup() con i file di font che si sa che si useranno, così da parsarli durante l’inizializzazione anziché al primo render, poi lock() sul registry così che il suo stato parsato sia congelato e nessuna mutazione per invocazione possa creare una race:

use NextPDF\Typography\FontRegistry;
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
// Container-scoped, built once at cold start (module scope, not per request).
$fontsDir = __DIR__ . '/resources/fonts';
$registry = new FontRegistry($fontsDir);
// Parse the fonts you will actually use now, so the first render does not.
$registry->warmup([
$fontsDir . '/liberation/LiberationSans-Regular.ttf',
$fontsDir . '/liberation/LiberationSans-Bold.ttf',
]);
// Freeze the parsed state for the life of the warm container.
$registry->lock();
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
// Each invocation: fresh document from the shared, warm factory.
$handler = static function (array $event) use ($factory): string {
$document = $factory->create();
$document->addPage();
$document->cell(0, 10, 'Hello from serverless', newLine: true);
return $document->getPdfData();
};

Chiamare warmup() prima di lock() — il registry è congelato una volta bloccato, quindi un warmup dopo di esso solleva un errore di configurazione. Trattare un font che non riesce a caricarsi al warmup come un errore in fase di deploy, non un dettaglio di runtime: validare che ogni percorso di font che si intende riscaldare esista e venga parsato effettivamente all’avvio, e far fallire il deploy (o il proprio health check) se uno non lo fa, anziché lasciare che un percorso digitato male emerga più tardi come glifi mancanti. Mantenere l’elenco di warmup ai font di cui una invocazione tipica ha bisogno; riscaldare una famiglia ampia che si usa raramente allunga solo ogni avvio a freddo.

Bref fornisce il runtime PHP per Lambda come layer pubblicato e plugin serverless.yml. Il runtime php-84 contiene già le estensioni di cui nextpdf/core ha bisogno, quindi si distribuiscono il proprio codice e i font e si punta una funzione a un handler. Un serverless.yml minimale:

service: nextpdf-serverless
provider:
name: aws
region: us-east-1
runtime: provided.al2023
plugins:
- ./vendor/bref/bref
functions:
generate:
handler: handler.php
description: Generate a PDF with the native NextPDF engine
runtime: php-84
memorySize: 1024 # size to the build; see "Sizing" below
timeout: 30 # seconds; raise for large documents
# The Lambda filesystem is read-only except /tmp. Fonts ship in the
# package under resources/fonts and are registered in the handler.

L’handler costruisce il documento con il factory caldo, con ambito al container, e restituisce i byte. Per un’API HTTP, restituirli codificati in base64 con il content type application/pdf così che API Gateway tratti il corpo come binario; per un trigger di invocazione o di coda, caricare i byte nell’object storage e restituire la chiave:

handler.php (outline)
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
use NextPDF\Typography\FontRegistry;
// --- Cold-start: built once per container, reused across warm invocations. ---
$fontsDir = __DIR__ . '/resources/fonts';
$registry = new FontRegistry($fontsDir);
$registry->warmup([$fontsDir . '/liberation/LiberationSans-Regular.ttf']);
$registry->lock();
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
// --- Per-invocation handler. ---
return static function (array $event) use ($factory): array {
$document = $factory->create();
$document->addPage();
$document->cell(0, 10, 'Invoice', newLine: true);
// getPdfData() materializes the whole PDF in memory and returns it.
$bytes = $document->getPdfData();
return [
'statusCode' => 200,
'isBase64Encoded' => true,
'headers' => ['Content-Type' => 'application/pdf'],
'body' => base64_encode($bytes),
];
};

Verificare che il pacchetto contenga un ambiente sano prima di instradarvi il traffico. nextpdf/core include una CLI installata in vendor/bin/nextpdf il cui comando doctor riporta esattamente sulle estensioni di cui il motore ha bisogno. Eseguirlo una volta sulla stessa immagine o layer del runtime per confermare che PHP 8.4 e ogni estensione richiesta siano presenti.

Cloud Run e App Runner eseguono un container anziché una funzione zippata, quindi la build è l’immagine Docker da Containerizzare un’applicazione NextPDF, non un pacchetto Bref. I vincoli del motore nativo sono identici: includere i font nell’immagine, registrare la directory inclusa in PHP, eseguire senza privilegi e trattare il filesystem come non durevole. A differenza del modello solo-/tmp di Lambda, un container Cloud Run / App Runner ha un filesystem scrivibile effimero, con ambito al container — ma viene azzerato a ogni riciclo, quindi usare /tmp (un tmpfs su Cloud Run) o un volume configurato per lo scratch e non fare mai affidamento sulle scritture nel percorso dell’immagine dell’applicazione come storage durevole.

Le differenze rispetto a Lambda sono operative, non strutturali:

  • Il container può restare caldo tra le richieste sotto un’impostazione di concorrenza, quindi il warmup del FontRegistry/DocumentFactory con ambito al container qui sopra rende su molte richieste, non solo sull’invocazione successiva.
  • Si serve su HTTP (un FPM o il SAPI del server PHP integrato) anziché un evento di invocazione, quindi si restituiscono i byte tramite la risposta del proprio framework. Per un documento di grandi dimensioni, restituirli come una risposta in streaming — vedere Trasmettere in streaming un PDF generato di grandi dimensioni come risposta HTTP.
  • Il timeout della richiesta e la memoria sono impostati sul servizio (timeout/memoria del servizio Cloud Run; configurazione dell’istanza App Runner) anziché per funzione.

Tutto il resto — l’insieme delle estensioni, la registrazione dei font, la chiamata di output getPdfData() — è lo stesso codice dell’handler Lambda.

  • Dimensione del pacchetto e dell’immagine. L’artefatto porta vendor/ (solo produzione — installare con --no-dev) e i font inclusi. I font dominano: una famiglia CJK completa è decine di megabyte. Includere solo i font che si rende effettivamente per tenere il pacchetto Lambda sotto i suoi limiti e l’immagine piccola, il che accorcia anche gli avvii a freddo. La famiglia Liberation inclusa (resources/fonts/liberation/) è piccola e copre la sostituzione di Helvetica con compatibilità metrica.
  • Memoria. getPdfData() costruisce l’intero documento in memoria e lo restituisce come una singola stringa, quindi la memoria di picco è all’incirca la dimensione di un PDF finito più il working set della build. Dimensionare la memoria della funzione/container al documento più grande che si genera, non a una media. Su Lambda, la memoria scala anche la CPU, quindi più memoria spesso significa una build più veloce e un’esecuzione più economica nonostante il tasso per millisecondo più alto — misurare entrambi. Un documento di poche pagine è comodo a 512–1024 MB; documenti ricchi di immagini o con molte pagine ne richiedono di più.
  • Timeout. La build, non il trasferimento, domina il budget della richiesta. Impostare il timeout della funzione al di sopra del tempo di build nel caso peggiore con un margine. Se un documento è abbastanza grande da rischiare un timeout, spostare la generazione su un trigger asincrono (una Lambda dietro coda o un job Cloud Run) che scrive il risultato nell’object storage anziché bloccare una richiesta sincrona.
  • Dimensione di /tmp. Se si mette in staging qualcosa sotto /tmp, tenere conto del suo limite di dimensione e ricordare che persiste tra le invocazioni a caldo — ripulire, o un container a lunga durata lo riempie lentamente.
  • Nessun save() durevole nella directory dell’app. Il filesystem di distribuzione non è durevole — la directory dell’app di Lambda è in sola lettura (solo /tmp accetta scritture), e un filesystem di container Cloud Run / App Runner è scrivibile ma effimero. Usare getPdfData() e restituire/caricare i byte; mettere in staging sotto /tmp o un volume configurato se si deve.
  • Non fare affidamento sul discovery automatico dei font. Non installare font del sistema operativo a runtime, e non fare affidamento sul discovery automatico dei font; includere i propri file di font per la produzione. Il motore nativo non legge font OS/fontconfig — risolve solo i file che si registrano. Se si recuperano deliberatamente file di font in /tmp o in un volume configurato, si devono registrare esplicitamente con il font registry e accettare il costo aggiuntivo di avvio a freddo e di affidabilità. Includere e registrare i file. Vedere la pagina dei font collegata sopra.
  • NEXTPDF_FONTS_PATH non fa nulla per il motore nudo. È il valore predefinito di configurazione di nextpdf/laravel, non una variabile che nextpdf/core legge. Un handler Bref nudo che imposta solo quella variabile non registra alcun font e rende tofu.
  • Il bridge Chrome non è adatto a una funzione di base. writeHtmlChrome ha bisogno di un Chromium headless e del percorso del sottoprocesso symfony/process. Mettere Chromium su Lambda richiede un layer personalizzato con il browser e le sue librerie, pacchetti molto più grandi e lunghi avvii a freddo. Il motore nativo e writeHtml non hanno bisogno di nulla di tutto ciò — preferirli su serverless.
  • Il costo di avvio a freddo è l’autoload più il parsing dei font. Usare --optimize-autoloader sull’installazione di produzione e riscaldare il registry una volta per container. Non riscaldare font che si usano raramente.
  • API Gateway necessita di gestione binaria. Restituire isBase64Encoded: true con Content-Type: application/pdf, e configurare l’API perché tratti application/pdf come tipo di media binario, altrimenti il client riceve byte corrotti.
  • Premium e ionCube sono una preoccupazione di artefatto più pesante. Le build ionCube-encoded di NextPDF Pro / Enterprise necessitano del Loader ionCube abbinato all’esatta build PHP nel runtime, che un layer Bref di serie non include. Questo è fuori ambito per un deploy serverless core.
  • Non distribuire dipendenze di sviluppo. Installare con --no-dev così che il tooling di test e analisi non entri mai nel pacchetto o nell’immagine della funzione.
  • Validare l’input prima di costruire. Una build PDF pilotata dall’input della richiesta è un vettore di esaurimento della memoria; rifiutare input fuori intervallo o sovradimensionati al confine prima che venga eseguito qualsiasi lavoro di build, e limitare la concorrenza così che un traffico elevato non moltiplichi la memoria di picco in un fallimento out-of-memory.
  • Tenere font e licenze fuori dagli artefatti pubblici. Includere solo i font che si è autorizzati a incorporare, e non incorporare mai un file di licenza premium in un’immagine o un layer spinto pubblicamente — fornirlo a runtime tramite un valore d’ambiente o un secret manager.
  • Privilegio minimo. Concedere alla funzione/servizio solo i permessi IAM di cui ha bisogno (per esempio, accesso in scrittura all’unico bucket di output) ed eseguire il container senza privilegi come mostra la guida Docker.

Questa guida non avanza alcuna pretesa normativa rispetto agli standard. I fatti di piattaforma sono letti direttamente dal pacchetto nextpdf/core: il vincolo php: >=8.4 <9.0 e le estensioni richieste ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib e ext-curl. Il layer runtime PHP-8.4 standard di Bref le include tutte e sei; l’immagine ufficiale php:8.4 fornisce openssl, curl e zlib, ma mbstring, gd e intl devono essere installate e abilitate nella build dell’immagine con docker-php-ext-install (vedere la pagina Docker). La chiamata di output è la vera superficie core NextPDF\Core\Document::getPdfData(): string (la sua controparte su disco è save(string $path): void). I font sono registrati tramite NextPDF\Typography\FontRegistry — il suo argomento di directory del costruttore / addFontDirectory(), con warmup(array $fontFiles) e lock() per il pattern di avvio a freddo — cablato tramite NextPDF\Core\DocumentFactory::create(). NEXTPDF_FONTS_PATH è la chiave di configurazione fonts_path del pacchetto nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), non una variabile che nextpdf/core legge. Il comando doctor della CLI nextpdf è dichiarato come "bin": ["bin/nextpdf"] nel pacchetto e installato in vendor/bin/nextpdf in un’app che lo consuma. I nomi dei runtime Bref e i comportamenti di AWS Lambda / Cloud Run / App Runner sono le funzionalità documentate da quei fornitori.