Использование NextPDF для Laravel в продакшене
В продакшене получайте контракт документа через внедрение в конструктор. Для сбоев записи PDF используйте отдельное исключение. Тяжёлую или пакетную генерацию переносите в GeneratePdfJob и подключайте явные обратные вызовы при успехе и сбое.
Установка
Заголовок раздела «Установка»composer require nextpdf/laravelphp 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), а дорогостоящие ресурсы по-прежнему совместно используются в рамках обработчика.
В продакшен-коде предпочитайте внедрение через конструктор вместо фасада. Это делает зависимость явной и позволяет модульно тестировать контроллер без загрузки корня фасада.
Пример кода — продакшен
Заголовок раздела «Пример кода — продакшен»Контроллер с внедрением зависимостей и типизированной обработкой ошибок
Заголовок раздела «Контроллер с внедрением зависимостей и типизированной обработкой ошибок»<?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. Замыкание-построитель получает документ, разрешённый контейнером, и должно вернуть настроенный документ.
<?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(), который обработчик очереди вызывает при окончательном сбое. Обратные вызовы оборачиваются в сериализуемые замыкания, поэтому они сохраняются при передаче через транспорт очереди.
Настройка поведения очереди
Заголовок раздела «Настройка поведения очереди»| Свойство | По умолчанию | Ключ конфигурации |
|---|---|---|
tries | 3 | не управляется конфигурацией; переопределяется подклассом |
timeout | 120 | nextpdf.queue.timeout |
backoff | 10 | не управляется конфигурацией; переопределяется подклассом |
| имя очереди | pdf | nextpdf.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/ — типичные сбои в продакшене