Перейти к содержимому

Преобразование документов Office в PDF с помощью Gotenberg

Мост Gotenberg преобразует документы Office в PDF. Он отправляет документ в микросервис Gotenberg по HTTPS и возвращает байты PDF. Вы описываете службу неизменяемым объектом GotenbergConfig, подключаете клиент PSR-18 и фабрики PSR-17 к GotenbergBridge, проверяете доступность службы и преобразуете файл с диска или байты из памяти. В руководстве разобраны определение формата по расширению, проверка доступности, контракт типизированных отказов и передача результата на постобработку в NextPDF.

Сразу обозначим предварительные требования:

  • Ядро NextPDF и nextpdf/gotenberg установлены.
  • Служба Gotenberg доступна по HTTPS. Мост отклоняет URL вида http:// до отправки любого запроса за пределы процесса.
  • Клиент PSR-18 и фабрики запросов и потоков PSR-17 установлены. Для закрепления DNS и TLS также предоставьте фабрику ответов PSR-17.
  • Входные данные должны быть в одном из шести распознаваемых форматов Office: .docx, .xlsx, .pptx, .odt, .ods или .odp. Любое другое расширение мост отклоняет с ошибкой ValueError.

Это практическое руководство. Полную готовую к запуску программу см. в кратком руководстве по Gotenberg.

Установите мост, клиент PSR-18 и фабрики PSR-17.

Окно терминала
composer require nextpdf/gotenberg guzzlehttp/guzzle

Запустите службу Gotenberg, доступную по HTTPS. Bearer-токен получайте из менеджера секретов или из внедрённого значения окружения. Мост никогда не читает переменные окружения и никогда не создаёт HTTP-клиент; оба компонента предоставляете вы.

GotenbergBridge::convertFile() принимает путь к файлу на диске. Метод приводит путь к каноническому виду, чтобы заблокировать обход каталогов, сопоставляет расширение файла с поддерживаемым форматом, проверяет размер и имя файла и отправляет multipart-запрос на <apiUrl>/forms/libreoffice/convert. convertString() выполняет те же шаги для байтов, которые у вас уже есть; метод использует исходное имя файла, чтобы определить расширение.

Формат определяется по расширению. Мост сопоставляет .docx, .xlsx, .pptx, .odt, .ods и .odp с их форматами и отклоняет всё остальное с ошибкой ValueError до отправки сетевого трафика. В объекте результата исходный формат доступен как значение перечисления.

Мост выполняет один синхронный HTTP-обмен с проверками до и после него. Он не выполняет повторные попытки, не ставит запросы в очередь, не кэширует и не ограничивает частоту; эти механизмы относятся к приложению вокруг моста. Относитесь к каждому преобразованию как к удалённому вызову службы, которую вы эксплуатируете, но не контролируете внутри процесса, и проектируйте с учётом её задержек и режимов отказа.

Мост сообщает об отказах через типизированные исключения и никогда не возвращает частичный или непроверенный результат:

  • Любой статус, кроме 200, Content-Type без application/pdf или тело, которое не начинается с %PDF, вызывают GotenbergConvertException. Мост возвращает результат только тогда, когда все три проверки пройдены.
  • Сбой клиента PSR-18, включая сетевой сбой или тайм-аут, оборачивается в GotenbergConvertException с исходным исключением в качестве причины.
  • Сбои проверки (URL без HTTPS, частный или зарезервированный адрес, слишком большой входной файл, небезопасное имя файла) вызывают RuntimeException до отправки сетевого трафика.
  • Нераспознанное расширение файла вызывает ValueError до отправки сетевого трафика.
// 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

В объекте результата доступны pdfData, перечисление sourceFormat, isValid() (true, когда тело непустое и начинается с %PDF) и size(). Полный справочник полей, карту ключей fromArray() и правила выбора транспорта смотрите на странице конфигурации Gotenberg, ссылка на которую приведена в разделе “Смотрите также”.

Опишите службу, подключите мост, проверьте его и преобразуйте один файл.

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);

Класс — NextPDF\Gotenberg\GotenbergConfig (выше указано точное пространство имён, которое должен импортировать ваш код). isAvailable() возвращает false и никогда не выбрасывает исключение для пустого URL, URL без HTTPS, URL с частным адресом или при любой сетевой ошибке; ответ /health со статусом ниже 500 означает доступность.

В продакшен-преобразовании каждый тип отказа перехватывается отдельно, повторная попытка выполняется только при подходящих условиях, а параллелизм ограничивается на стороне вызывающего кода. Ниже показан полный порядок перехвата.

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;
}
}

Повторяйте попытку только при GotenbergConvertException транспортного уровня (обёрнутое исключение клиента PSR-18) и при идемпотентных серверных ошибках (502, 503, 504). Ответ класса 400 обычно означает, что входные данные неверны, поэтому повторная попытка завершится так же. Ограничьте общее число попыток и суммарное затраченное время. Число одновременных преобразований ограничивайте пропускной способностью, которую выдерживает ваше развёртывание Gotenberg. Сам мост не хранит состояние и безопасен для использования из множества обработчиков, но пропускная способность преобразования у службы ограничена.

  • Определение формата выполняется по расширению. Файл .docx, переименованный в .txt, отклоняется с ошибкой ValueError; файл .txt, переименованный в .docx, отправляется в Gotenberg и завершается ошибкой там. Принимая загружаемые файлы, проверяйте реальный формат, а не полагайтесь на имя.
  • fromArray() по замыслу терпим к входным данным. Он молча подставляет значения по умолчанию для некорректных входных данных. Проверяйте исходный массив на этапе загрузки приложения, чтобы отсутствующий URL выявлялся рано как ошибка конфигурации, а не как исключение при каждом преобразовании.
  • Ограничение размера срабатывает внутри процесса. maxFileSize (по умолчанию 50 MiB) проверяется до отправки запроса, поэтому слишком большой файл никогда не расходует пропускную способность службы. Снизьте ограничение под реальные потребности ваших документов; меньшее ограничение — более дешёвая защита от отказа в обслуживании.
  • Проверка работоспособности не бесплатна. Вызывайте isAvailable() из эндпойнта готовности или работоспособности, а не перед каждым преобразованием. Её запуск при каждом преобразовании удваивает частоту запросов к службе без какой-либо пользы.
  • Кэширования внутри процесса нет. Если один и тот же документ преобразуется многократно, кэшируйте полученный PDF в своём приложении, используя в качестве ключа хеш содержимого входных данных.
  • renderTimeMs вы задаёте сами. В результате поле времени равно 0.0, если ваша интеграция не измеряет и не устанавливает его. Замеряйте время вызова самостоятельно, если вам нужно это число.

На время запроса преобразование удерживает одно соединение и один обработчик LibreOffice на стороне Gotenberg, а обработка Office-документов занимает время. Задавайте timeout с запасом, исходя из измеренной задержки преобразования для ваших реальных документов. Держите его ниже тайм-аута любого вышестоящего шлюза или PHP max_execution_time, чтобы тайм-аут сначала срабатывал в мосте и вы получали типизированное исключение вместо принудительно завершённого процесса. Ограничивайте параллелизм очередью, семафором или пулом обработчиков, размер которого соответствует пропускной способности службы. Кэша внутри процесса нет; добавьте его в своё приложение, если вы многократно преобразуете одни и те же входные данные.

  • Проверка HTTPS и фильтрация адресов до отправки. Мост отклоняет URL без HTTPS и адрес назначения, который разрешается в частное или зарезервированное адресное пространство, до выхода любого запроса за пределы процесса. При каждом повторном вызове эта проверка выполняется заново, поэтому повторная попытка не может обойти защиту от SSRF.
  • Транспорт с закреплением по запросу. Когда вы предоставляете фабрику ответов и закрепления (или задан набор разрешённых IP), мост привязывает соединение к разрешённым адресам, применяет закрепление SPKI, проверяет узел и хост, применяет тайм-аут и отключает переход по перенаправлениям. Настройте резервное закрепление перед ротацией сертификата.
  • Не доверяйте заявленному типу содержимого загружаемого файла. Принимая файлы от пользователей, проверяйте реальный тип файла самостоятельно; сопоставление расширения с форматом — это решение о маршрутизации, а не проверка подлинности.
  • Секреты скрыты и неизменяемы. apiKey помечен атрибутом #[SensitiveParameter], а конфигурация — final readonly. Получайте токен из менеджера секретов; никогда не сохраняйте его в системе контроля версий. Запись журнала о преобразовании содержит URL, имя файла, формат и длину содержимого — никогда содержимое файла или токен.
  • Никогда не пишите пустой блок catch. Каждый пример перехватывает конкретный тип и ведёт журнал с контекстом.

Полную модель безопасности и развёртывания смотрите на странице безопасности и эксплуатации Gotenberg. Контракт транспорта PSR-18 и рекомендация не доверять типу содержимого разобраны в соответствующих пунктах на странице продакшен-использования.

Это руководство не делает собственных нормативных заявлений о соответствии стандартам. Поведение транспорта PSR-18 в мосте (клиент выбрасывает исключение только тогда, когда не может отправить запрос или разобрать ответ; 4xx/5xx — это обычное возвращаемое значение), рекомендации по проверке загружаемых файлов и модель закрепления TLS привязаны к PSR-18, OWASP и RFC 7469 на вышестоящих страницах продакшен-использования и конфигурации Gotenberg. Эта страница сборника рецептов повторяет сценарий использования и оставляет эти ссылки на тех страницах. Мост производит байты PDF и на этом останавливается. Подписание, профили PDF/A и водяные знаки относятся к постобработке в NextPDF и являются возможностью коммерческой редакции, а не частью этого моста.