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

Использование NextPDF для Laravel в продакшене

В продакшене получайте контракт документа через внедрение в конструктор. Для сбоев записи PDF используйте отдельное исключение. Тяжёлую или пакетную генерацию переносите в GeneratePdfJob и подключайте явные обратные вызовы при успехе и сбое.

Окно терминала
composer require nextpdf/laravel
php artisan vendor:publish --tag=nextpdf-config

Настройте подключение очереди в config/nextpdf.php. Задайте queue.connection, queue.queue и queue.timeout. Затем убедитесь, что обработчик запущен на настроенном подключении.

Контейнер предоставляет NextPDF\Contracts\PdfDocumentInterface как фабричную привязку. При каждом разрешении возвращается новый NextPDF\Core\Document. PSR-11 допускает, что контейнер возвращает разные значения при последовательных вызовах get() в зависимости от стратегии привязки (PSR-11 §1.1.2). Этот пакет использует фабричную привязку, поэтому изменяемое состояние запроса никогда не переходит между запросами. Реестры шрифтов и изображений являются синглтонами. Это сохраняет контракт: привязанный идентификатор разрешается в свою зарегистрированную запись (PSR-11 §1.1.2), а дорогостоящие ресурсы по-прежнему совместно используются в рамках обработчика.

В продакшен-коде предпочитайте внедрение через конструктор вместо фасада. Это делает зависимость явной и позволяет модульно тестировать контроллер без загрузки корня фасада.

Контроллер с внедрением зависимостей и типизированной обработкой ошибок

Заголовок раздела «Контроллер с внедрением зависимостей и типизированной обработкой ошибок»
resource: NextPDF\Contracts\PdfDocumentInterface + src/Laravel/Http/PdfResponse.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Response;
use NextPDF\Contracts\PdfDocumentInterface;
use NextPDF\Laravel\Http\PdfResponse;
use Psr\Log\LoggerInterface;
use Throwable;
final class InvoiceController extends Controller
{
public function __construct(
private readonly PdfDocumentInterface $document,
private readonly LoggerInterface $logger,
) {}
public function show(int $invoiceId): Response
{
try {
$this->document->addPage();
$this->document->cell(0, 10, "Invoice #{$invoiceId}", newLine: true);
$this->document->cell(0, 10, 'Thank you for your business.');
return PdfResponse::download(
$this->document,
"invoice-{$invoiceId}.pdf",
);
} catch (Throwable $exception) {
// Rethrow as an HTTP-meaningful failure; never swallow.
$this->logger->error('Invoice PDF generation failed', [
'invoice_id' => $invoiceId,
'exception' => $exception::class,
]);
return new Response('Could not generate the invoice PDF.', 500);
}
}
}

Внедряйте PdfDocumentInterface, а не конкретный Document, чтобы можно было подменять привязку в тестах. Контейнер возвращает новый документ при каждом создании экземпляра контроллера. Не используйте повторно один и тот же экземпляр контроллера для двух несвязанных документов в рамках одного процесса.

Блок catch логирует класс исключения и возвращает явную ошибку HTTP вместо раскрытия трассировки стека. Используйте Psr\Log\LoggerInterface, который контейнер разрешает как логгер фреймворка. PSR-3 возлагает экранирование заполнителей на реализацию и предписывает вызывающему коду не экранировать значения контекста заранее (PSR-3 §1.2). Передавайте структурированный контекст, а не интерполированные строки.

Генерация в очереди с обратными вызовами успеха и сбоя

Заголовок раздела «Генерация в очереди с обратными вызовами успеха и сбоя»

GeneratePdfJob — задание ShouldQueue. По умолчанию у него три попытки, тайм-аут 120 секунд и задержка 10 секунд. Все три значения можно переопределить в config/nextpdf.php. Замыкание-построитель получает документ, разрешённый контейнером, и должно вернуть настроенный документ.

resource: src/Laravel/Jobs/GeneratePdfJob.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
{
// Dispatchable::dispatch() is `public static`: it constructs the
// job from the arguments it receives and returns a PendingDispatch.
// Pass every constructor argument — including the callbacks — to
// the static call. Building an instance and then calling
// `$job->dispatch(...)` would discard that instance (and its
// callbacks) and queue a different job from only the static args.
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,
]);
},
);
}
}

GeneratePdfJob::dispatch() передаёт свои аргументы напрямую в конструктор (string $outputPath, callable $builder, ?callable $onSuccess, ?callable $onFailure). Поэтому обратные вызовы при успехе и сбое подключаются к тому же заданию, которое ставится в очередь. Это соответствует позиционной форме GeneratePdfJob::dispatch($path, $builder) в /integrations/laravel/quickstart/. Обратный вызов при успехе получает путь к выходному файлу, а обратный вызов при сбое получает Throwable. Задание также предоставляет цепочечные сеттеры then() и catch(), которые возвращают задание для построения цепочки. Используйте эти сеттеры только тогда, когда сохраняете и отправляете в очередь тот же экземпляр, например через помощник dispatch(). Задание также предоставляет метод failed(), который обработчик очереди вызывает при окончательном сбое. Обратные вызовы оборачиваются в сериализуемые замыкания, поэтому они сохраняются при передаче через транспорт очереди.

СвойствоПо умолчаниюКлюч конфигурации
tries3не управляется конфигурацией; переопределяется подклассом
timeout120nextpdf.queue.timeout
backoff10не управляется конфигурацией; переопределяется подклассом
имя очередиpdfnextpdf.queue.queue
подключениепо умолчаниюnextpdf.queue.connection

tries и backoff — публичные свойства, которые читаются из экземпляра задания. Поставляемая реализация задания не читает их из конфигурации. Если у вас другая политика повторов, создайте подкласс GeneratePdfJob, чтобы переопределить их.

  • Замыкание-построитель должно возвращать PdfDocumentInterface. Задание сохраняет именно возвращённое значение, а не изначально разрешённый экземпляр. Тест задания явно проверяет этот контракт.
  • Разрешение SignerInterface возвращает null, если хотя бы одно из условий не выполнено: подписание включено, сертификат настроен и установлен nextpdf/premium. Всегда проверяйте значение на null перед подписанием.
  • Долгоживущие обработчики (Octane/RoadRunner/Swoole) совместно используют заблокированный реестр шрифтов. Настройте preload_fonts, чтобы прогрев выполнялся один раз при запуске обработчика, а не при первом запросе.
  • Задание со сбоем вызывает failed() после исчерпания tries. Сбой отдельной попытки не вызывает onFailure, пока обработчик очереди не объявит окончательный сбой.

Синхронная генерация в контроллере блокирует запрос на всё время построения PDF. Для многостраничного или пакетного вывода отправляйте GeneratePdfJob в очередь и сразу возвращайте управление. Реестры-синглтоны распределяют затраты на разбор шрифтов и декодирование изображений по всему времени жизни обработчика. Тогда затраты на запрос ограничиваются построением документа и выводом содержимого.

Контроллер с внедрением зависимостей логирует класс исключения, а не его сообщение или трассировку, чтобы не раскрывать внутренние детали в логах. GeneratePdfJob проверяет путь к выходному файлу на стороне обработчика, чтобы снизить риск поддельных сериализованных полезных нагрузок в транспорте очереди. Полное описание см. в /integrations/laravel/security-and-operations/.

УтверждениеИсточникПунктreference_id (идентификатор ссылки)
Привязанный идентификатор разрешается в соответствующую зарегистрированную записьКонтейнер PSR-11§1.1.2
Последовательные разрешения могут различаться в зависимости от стратегии привязки (фабричная привязка)Контейнер PSR-11§1.1.2

Рекомендации по логированию приведены в спецификации PSR-3. Спецификация возлагает экранирование заполнителей на реализацию и предписывает вызывающему коду передавать структурированный контекст. См. документ psr_3_logger §1.2.

Подписанный вывод PAdES B-B и архивирование PDF/A через nextpdf/premium используют ту же API-поверхность внедрения зависимостей (DI). Это дополнительная возможность Enterprise. Для её использования в описанном здесь пакете Core не нужны изменения в коде. См. https://nextpdf.dev/get-license/?intent=laravel-signing.

  • /integrations/laravel/quickstart/ — минимальный вводный пример
  • /integrations/laravel/configuration/ — ключи для очереди, подписи и шрифтов
  • /integrations/laravel/security-and-operations/ — модель угроз и усиление защиты
  • /integrations/laravel/troubleshooting/ — типичные сбои в продакшене