Aller au contenu

Convertir des documents Office en PDF avec Gotenberg

Le pont Gotenberg convertit un document Office en PDF. Il envoie le document à un microservice Gotenberg via HTTPS et renvoie les octets du PDF obtenu. Configure le service avec un GotenbergConfig immuable, branche un client PSR-18 et des fabriques PSR-17 dans GotenbergBridge, vérifie sa disponibilité, puis convertis un fichier depuis le disque ou des octets en mémoire. Ce guide couvre la détection du format à partir de l’extension du fichier, le contrôle de disponibilité, le contrat d’échec typé et le passage de relais vers le post-traitement NextPDF.

Prérequis, énoncés d’emblée :

  • Le cœur de NextPDF et nextpdf/gotenberg sont installés.
  • Un service Gotenberg est accessible via HTTPS. Le pont rejette une URL en clair http:// avant qu’aucune requête ne quitte le processus.
  • Un client PSR-18 ainsi que des fabriques de requête et de flux PSR-17 sont installés. Pour l’épinglage DNS et TLS, tu fournis aussi une fabrique de réponse PSR-17.
  • L’entrée utilise l’un des six formats Office reconnus : .docx, .xlsx, .pptx, .odt, .ods ou .odp. Le pont rejette toute autre extension avec une ValueError.

Ce document est un guide pratique. Pour un programme complet et exécutable, lis le démarrage rapide Gotenberg.

Installe le pont ainsi qu’un client PSR-18 et des fabriques PSR-17.

Fenêtre de terminal
composer require nextpdf/gotenberg guzzlehttp/guzzle

Exécute un service Gotenberg accessible via HTTPS et récupère tout jeton bearer depuis un gestionnaire de secrets ou une valeur d’environnement injectée. Le pont ne lit jamais de variables d’environnement et ne construit jamais de client HTTP ; tu fournis les deux.

GotenbergBridge::convertFile() prend un chemin sur le disque. Il résout le chemin canonique pour bloquer la traversée de répertoire, associe l’extension du fichier à un format pris en charge, contrôle la taille et le nom de fichier, puis envoie une requête multipart à <apiUrl>/forms/libreoffice/convert. convertString() fait de même pour des octets que tu détiens déjà ; il utilise le nom de fichier d’origine afin que l’extension puisse être détectée.

La détection du format se fait par l’extension. Le pont associe .docx, .xlsx, .pptx, .odt, .ods et .odp à leurs formats respectifs et rejette tout le reste avec une ValueError avant tout trafic réseau. L’objet résultat expose le format source détecté sous forme de valeur d’énumération.

Le pont effectue un seul aller-retour HTTP synchrone entouré de validation. Il ne réessaie pas, ne met pas en file d’attente, ne met pas en cache et ne limite pas le débit ; c’est à l’application qui l’entoure de le faire. Traite chaque conversion comme un appel distant vers un service que tu exploites mais ne contrôles pas dans le processus, et conçois ton intégration en tenant compte de sa latence et de ses défaillances.

Le pont fait remonter les échecs sous forme d’exceptions typées et ne renvoie jamais de résultat partiel ou non validé :

  • Chacun des cas suivants lève une GotenbergConvertException : un statut différent de 200, un Content-Type sans application/pdf, ou un corps qui ne commence pas par %PDF. Le pont ne renvoie un résultat que lorsque les trois contrôles réussissent.
  • Un échec du client PSR-18, y compris une panne réseau ou un délai d’attente dépassé, est enveloppé dans une GotenbergConvertException qui conserve l’exception d’origine comme cause.
  • Les échecs de validation (URL non-HTTPS, adresse privée ou réservée, entrée surdimensionnée, nom de fichier dangereux) lèvent une RuntimeException avant tout trafic réseau.
  • Une extension de fichier non reconnue lève une ValueError avant tout trafic réseau.
// Configuration (final readonly):
new GotenbergConfig(
string $apiUrl, // required, must be HTTPS
int $timeout = 30, // hard transfer timeout, seconds
int $maxFileSize = 52_428_800, // 50 MiB
string $apiKey = '', // #[SensitiveParameter]; Bearer when non-empty
list<string> $pinnedPublicKeys = [], // sha256/<base64>
list<string> $backupPublicKeys = [],
)
GotenbergConfig::fromArray(array $config): self
GotenbergConfig::isValid(): bool
// The bridge:
new GotenbergBridge(
GotenbergConfig $config,
ClientInterface $httpClient, // PSR-18
RequestFactoryInterface $requestFactory, // PSR-17
StreamFactoryInterface $streamFactory, // PSR-17
?LoggerInterface $logger = null, // PSR-3
?HtmlSecurityPolicyInterface $htmlSecurityPolicy = null,
?ResponseFactoryInterface $responseFactory = null, // enables pinned transport
)
GotenbergBridge::isAvailable(): bool
GotenbergBridge::convertFile(string $path): GotenbergConvertResult
GotenbergBridge::convertString(string $bytes, string $originalFilename): GotenbergConvertResult

L’objet résultat expose pdfData, l’énumération sourceFormat, isValid() (true lorsque le corps est non vide et commence par %PDF) et size(). Pour la référence complète des champs, la table des clés fromArray() et les règles de sélection du transport, consulte la page de configuration Gotenberg référencée dans Voir aussi.

Configure le service, branche le pont, vérifie sa disponibilité et convertis un fichier.

convert-quickstart.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Gotenberg\GotenbergBridge;
use NextPDF\Gotenberg\GotenbergConfig;
use NextPDF\Gotenberg\GotenbergConvertException;
$config = new GotenbergConfig(
apiUrl: 'https://gotenberg.example.com',
timeout: 60,
apiKey: getenv('GOTENBERG_TOKEN') ?: '',
);
$bridge = new GotenbergBridge(
config: $config,
httpClient: $httpClient, // your PSR-18 client
requestFactory: $requestFactory, // your PSR-17 factory
streamFactory: $streamFactory, // your PSR-17 factory
responseFactory: $responseFactory, // enables the pinned transport
);
// Probe before converting. The probe validates the URL with no network
// traffic, then sends a HEAD to <apiUrl>/health.
if (!$bridge->isAvailable()) {
throw new RuntimeException('Gotenberg is not reachable.');
}
try {
$result = $bridge->convertFile('/path/to/report.docx');
} catch (GotenbergConvertException $exception) {
// Bad config, HTTP failure, non-200, wrong Content-Type, or non-PDF body.
throw $exception;
}
if (!$result->isValid()) {
throw new RuntimeException('Result is not a valid PDF.');
}
file_put_contents('/path/to/report.pdf', $result->pdfData);

La classe est NextPDF\Gotenberg\GotenbergConfig (la ligne ci-dessus utilise l’espace de noms exact que ton code doit importer). isAvailable() renvoie false et ne lève jamais d’exception lorsque l’URL est vide, non-HTTPS ou pointe vers une adresse privée, ni en cas d’erreur réseau ; un statut inférieur à 500 renvoyé par /health indique que le service est disponible.

Une conversion en production intercepte chaque type d’échec séparément, ne réessaie que dans les bonnes conditions et borne la concurrence côté appelant. L’ordre de capture ci-dessous est exhaustif.

OfficeConverter.php
<?php
declare(strict_types=1);
use NextPDF\Gotenberg\GotenbergBridge;
use NextPDF\Gotenberg\GotenbergConvertException;
use Psr\Log\LoggerInterface;
use RuntimeException;
use ValueError;
final readonly class OfficeConverter
{
public function __construct(
private GotenbergBridge $bridge,
private LoggerInterface $logger,
) {}
public function convert(string $path): string
{
try {
$result = $this->bridge->convertFile($path);
} catch (GotenbergConvertException $exception) {
// Transport, non-200, wrong Content-Type, or non-PDF body.
// Retry only on transport-level or 502/503/504 causes, with
// bounded exponential backoff and jitter — never blind retries.
$this->logger->error('gotenberg.convert.failed', [
'path' => basename($path),
'exception' => $exception::class,
]);
throw $exception;
} catch (ValueError $exception) {
// Extension is not one of the six recognized Office formats.
$this->logger->warning('gotenberg.convert.unsupported_format', [
'path' => basename($path),
]);
throw $exception;
} catch (RuntimeException $exception) {
// Non-HTTPS URL, private address, oversized input, or unsafe name.
$this->logger->error('gotenberg.convert.rejected', [
'path' => basename($path),
'exception' => $exception::class,
]);
throw $exception;
}
if (!$result->isValid()) {
throw new RuntimeException('Gotenberg returned an invalid PDF body.');
}
return $result->pdfData;
}
}

Ne relance un essai que pour une GotenbergConvertException de niveau transport (une exception du client PSR-18 enveloppée) et pour les erreurs serveur idempotentes (502, 503, 504). Une réponse de la classe 400 signifie généralement que l’entrée est incorrecte ; un nouvel essai échoue donc de la même manière. Plafonne le nombre total de tentatives et la durée totale d’exécution. Borne le nombre de conversions en cours à la capacité que ton déploiement Gotenberg supporte. Le pont lui-même est sans état et peut être utilisé en toute sécurité depuis de nombreux workers, mais le service a une capacité de conversion finie.

  • La détection du format se fait par l’extension. Un .docx renommé en .txt est rejeté avec une ValueError ; un .txt renommé en .docx est envoyé à Gotenberg et y échoue. Lorsque tu acceptes des téléversements, appuie-toi sur le format réel, pas sur le nom.
  • fromArray() est tolérante par conception. Elle remplace silencieusement une entrée mal formée par des valeurs par défaut. Valide le tableau source dans ton chemin de démarrage pour qu’une URL manquante apparaisse tôt comme une erreur de configuration, et non comme une exception par conversion.
  • Le plafond de taille est appliqué dans le processus. maxFileSize (50 Mio par défaut) est vérifié avant l’envoi de la requête, de sorte qu’un fichier surdimensionné ne consomme jamais de capacité du service. Abaisse le plafond pour qu’il corresponde aux besoins de tes documents ; un plafond plus bas est un contrôle anti-déni-de-service moins coûteux.
  • Le contrôle de disponibilité n’est pas gratuit. Appelle isAvailable() depuis un point de terminaison de disponibilité ou de santé, pas avant chaque conversion. L’exécuter à chaque conversion double le nombre de requêtes envoyées au service sans apporter de bénéfice.
  • Aucune mise en cache dans le processus. Si le même document est converti à répétition, mets en cache le PDF obtenu dans ton application, indexé par un hachage de contenu de l’entrée.
  • renderTimeMs est à ta charge. Le champ de chronométrage du résultat vaut 0.0 tant que ton intégration ne le mesure pas et ne le définit pas. Chronomètre l’appel toi-même si tu as besoin de la valeur.

Pendant la durée de la requête, une conversion mobilise une connexion et un worker LibreOffice côté Gotenberg, et la conversion Office n’est pas instantanée. Règle timeout à partir de la latence de conversion mesurée pour tes documents réels, avec une marge. Garde cette valeur en dessous du délai de toute passerelle en amont ou du max_execution_time de PHP, afin que le pont expire en premier et que tu obtiennes une exception typée plutôt qu’un processus tué. Borne la concurrence avec une file d’attente, un sémaphore ou un pool de workers dimensionné selon la capacité du service. Il n’y a aucun cache dans le processus ; ajoutes-en un dans ton application si tu convertis la même entrée à répétition.

  • HTTPS et filtrage des adresses avant l’envoi. Le pont rejette une URL non-HTTPS et une destination qui se résout dans l’espace d’adressage privé ou réservé avant qu’aucune requête ne quitte le processus. Chaque nouvel essai réexécute cette validation, de sorte qu’il ne peut pas contourner la protection SSRF.
  • Transport épinglé à la demande. Lorsque tu fournis une fabrique de réponse et des épingles (ou qu’il existe un ensemble d’IP résolues), le pont lie la connexion aux adresses résolues, applique l’épinglage SPKI, vérifie le pair et l’hôte, applique le délai d’attente et désactive le suivi des redirections. Configure une épingle de secours avant toute rotation de certificat.
  • Ne fais pas confiance au type de contenu déclaré d’un téléversement. Lorsque tu acceptes des téléversements d’utilisateurs, valide toi-même le vrai type de fichier ; le mappage extension-vers-format est une décision de routage, pas un contrôle d’authenticité.
  • Les secrets sont expurgés et immuables. apiKey porte #[SensitiveParameter], et la configuration est final readonly. Récupère le jeton depuis un gestionnaire de secrets ; ne le commite jamais. L’entrée de conversion journalisée contient l’URL, le nom de fichier, le format et la longueur du contenu — jamais le contenu du fichier ni le jeton.
  • N’écris jamais un bloc catch vide. Chaque exemple attrape le type spécifique et journalise avec du contexte.

Pour le modèle complet de sécurité et de déploiement, consulte la page sécurité et exploitation de Gotenberg. Le contrat de transport PSR-18 et les conseils invitant à ne pas faire confiance au type de contenu sont rattachés à leurs clauses sur la page d’usage en production en amont.

Ce guide ne fait aucune revendication normative de conformité propre. Le comportement du transport PSR-18 du pont (un client ne lève une exception que lorsqu’il ne peut ni envoyer ni analyser une réponse ; une réponse 4xx/5xx est une valeur de retour normale), les conseils de validation des téléversements de fichiers et le modèle d’épinglage TLS sont rattachés à PSR-18, à l’OWASP et à RFC 7469 sur les pages d’usage en production et de configuration Gotenberg en amont. Cette page du Cookbook reformule l’usage et renvoie vers ces citations sur les pages concernées. Le pont produit des octets PDF et s’arrête là. La signature, les profils PDF/A et le filigrane relèvent du post-traitement NextPDF et de l’édition commerciale ; ils ne font pas partie de ce pont.