Генерация PDF в задаче очереди
Ресурсоёмкая генерация PDF не должна выполняться в потоке запроса. Каждая интеграция с фреймворком предоставляет API для генерации через очередь: PDF строится и сохраняется на воркере. HTTP-запрос может завершиться сразу после того, как вы поставите задачу в очередь. В этом руководстве рассматривается вариант с очередью для Laravel (GeneratePdfJob), Symfony (GeneratePdfMessage через Messenger) и CodeIgniter 4 (GeneratePdfJob через codeigniter4/queue).
Предварительные требования:
- Установлены ядро NextPDF и одна из интеграций с фреймворком.
- Настроен транспорт воркера: подключение очереди Laravel, транспорт Symfony Messenger или очередь CodeIgniter 4 с установленным
codeigniter4/queue. - Для этого транспорта запущен процесс воркера.
В этом руководстве предполагается, что в вашем приложении уже настроена очередь. Для настройки очереди или Messenger используйте документацию вашего фреймворка.
Установка
Заголовок раздела «Установка»Установите интеграцию, затем зависимость очереди, необходимую вашему фреймворку.
composer require nextpdf/laravelcomposer require nextpdf/symfony symfony/messengerДля CodeIgniter нужен пакет очереди. Интеграция объявляет его как зависимость только для разработки, поэтому подключите его в приложении, где запускаются воркеры.
composer require nextpdf/codeigniter codeigniter4/queueВ Laravel настройте подключение очереди в config/nextpdf.php (queue.connection, queue.queue, queue.timeout), затем запустите воркер для этого подключения.
Концептуальный обзор
Заголовок раздела «Концептуальный обзор»Каждая интеграция использует общий подход в стиле своего фреймворка:
- Laravel предоставляет
NextPDF\Laravel\Jobs\GeneratePdfJob— задачуShouldQueue. Вы ставите её в очередь, передавая выходной путь и замыкание-построитель. Замыкание получает документ, разрешённый контейнером, и возвращает настроенный документ. На воркере задача сохраняет возвращённый документ по указанному пути. Она также принимает необязательные колбэки успеха и сбоя. - Symfony предоставляет
NextPDF\Symfony\Message\GeneratePdfMessage—readonly-сообщение для отправки в шину Messenger, а такжеGeneratePdfHandler. Обработчик разрешает построитель по имени класса из локатора сервисов PSR-11. Вы реализуетеNextPDF\Symfony\Message\PdfBuilderInterfaceдля каждого типа документа. - CodeIgniter 4 предоставляет
NextPDF\CodeIgniter\Jobs\GeneratePdfJob, зарегистрированный под именем-ключом вConfig\Queue::$jobHandlers. Вы помещаете задачу по её зарегистрированному имени, передавая ссылку на построитель, выходной путь и массив контекста. Построитель — это статический метод, ограниченный пространством имёнApp\PdfBuilders.
У всех трёх интеграций единый подход к безопасности: они проверяют выходной путь. Symfony и CodeIgniter повторно проверяют его при потреблении, поскольку полезная нагрузка может ожидать в очереди между постановкой в очередь и выполнением. На воркере построитель работает с новым документом, поэтому параллельные задачи никогда не используют общее состояние документа.
Поверхность API
Заголовок раздела «Поверхность API»| Аспект | Laravel | Symfony | CodeIgniter 4 |
|---|---|---|---|
| Единица очереди | GeneratePdfJob (ShouldQueue) | GeneratePdfMessage (DTO) + GeneratePdfHandler | GeneratePdfJob (обработчик очереди) |
| Постановка в очередь | GeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure) | MessageBusInterface::dispatch(new GeneratePdfMessage(...)) | service('queue')->push($queue, $name, $data) |
| Форма построителя | callable(PdfDocumentInterface): PdfDocumentInterface | PdfBuilderInterface::build(Document, array): Document | static fn(Document, array): Document в App\PdfBuilders |
| Защита пути / входных данных | Задача проверяет выходной путь на воркере | DTO проверяет путь при создании, обработчик повторно проверяет его при потреблении | Задача ограничивает путь каталогом WRITEPATH/pdfs/ и применяет список разрешённых пространств имён построителей |
| Обработка сбоев | failed() после исчерпания tries; onFailure при окончательном сбое | Стратегия повторов Messenger; типизированные ошибки проверки | InvalidArgumentException / QueueException |
Пример кода — быстрый старт
Заголовок раздела «Пример кода — быстрый старт»В каждом фреймворке можно использовать такую минимальную постановку в очередь.
<?php
declare(strict_types=1);
use NextPDF\Contracts\PdfDocumentInterface;use NextPDF\Laravel\Jobs\GeneratePdfJob;
GeneratePdfJob::dispatch( storage_path('app/reports/january-2026.pdf'), static fn (PdfDocumentInterface $document): PdfDocumentInterface => $document ->addPage() ->cell(0, 10, 'January report', newLine: true),);Выходной путь должен заканчиваться на .pdf; задача проверяет путь на воркере перед записью файла.
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Pdf\InvoicePdfBuilder;use NextPDF\Symfony\Message\GeneratePdfMessage;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Messenger\MessageBusInterface;use Symfony\Component\Routing\Attribute\Route;
final class ReportController{ #[Route('/invoice/{id}/queue', name: 'invoice_queue')] public function queue(MessageBusInterface $bus, int $id): Response { $bus->dispatch(new GeneratePdfMessage( builderClass: InvoicePdfBuilder::class, outputPath: '/var/storage/invoices/' . $id . '.pdf', builderContext: ['invoice_id' => $id], ));
return new Response('PDF generation queued.', 202); }}<?php
declare(strict_types=1);
namespace App\Controllers;
use CodeIgniter\HTTP\ResponseInterface;
final class InvoiceController extends BaseController{ public function queueInvoice(int $id): ResponseInterface { service('queue')->push('pdf-queue', 'generate-pdf', [ 'builder' => 'App\\PdfBuilders\\InvoiceBuilder::build', 'outputPath' => WRITEPATH . 'pdfs/invoice-' . $id . '.pdf', 'context' => ['invoice_id' => $id], ]);
return $this->response ->setStatusCode(ResponseInterface::HTTP_ACCEPTED) ->setJSON(['status' => 'queued', 'invoice_id' => $id]); }}В CodeIgniter передавайте в очередь ключ из jobHandlers ('generate-pdf'), а не строку класса задачи. Сначала зарегистрируйте обработчик в app/Config/Queue.php.
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Queue\Config\Queue as BaseQueue;use NextPDF\CodeIgniter\Jobs\GeneratePdfJob;
final class Queue extends BaseQueue{ /** @var array<string, class-string> */ public array $jobHandlers = [ 'generate-pdf' => GeneratePdfJob::class, ];}Пример кода — продакшен
Заголовок раздела «Пример кода — продакшен»В продакшене постановка в очередь связывает с логгером PSR-3 колбэки успеха и сбоя (Laravel) либо явно зарегистрированный построитель и типизированный обработчик (Symfony). Ниже пример для Laravel ставит задачу в очередь с обоими колбэками.
<?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 { // dispatch() is public static: it constructs the job from the // arguments it receives. Pass every argument — including the // callbacks — to the static call, not to a separately built instance. 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, ]); }, ); }}Колбэк успеха получает выходной путь. Колбэк сбоя получает Throwable. Сначала задача исчерпывает tries (по умолчанию 3), и только потом запускается обработка сбоя. Настройте timeout через nextpdf.queue.timeout. Значения tries и backoff — публичные свойства, поэтому создайте подкласс GeneratePdfJob, чтобы изменить их.
Для Symfony реализуйте построитель и зарегистрируйте его в локаторе сервисов. Так обработчику будут доступны только зарегистрированные построители.
<?php
declare(strict_types=1);
namespace App\Pdf;
use NextPDF\Core\Document;use NextPDF\Symfony\Message\PdfBuilderInterface;
final class InvoicePdfBuilder implements PdfBuilderInterface{ /** @param array<string, mixed> $context */ public function build(Document $document, array $context): Document { $document->addPage(); $document->setFont('dejavusans', '', 12); $document->cell(0, 10, 'Invoice #' . $context['invoice_id']);
return $document; }}services: App\Pdf\InvoicePdfBuilder: ~
nextpdf.pdf_builder_locator: class: Symfony\Component\DependencyInjection\ServiceLocator arguments: - 'App\Pdf\InvoicePdfBuilder': '@App\Pdf\InvoicePdfBuilder' tags: ['container.service_locator']
NextPDF\Symfony\Message\GeneratePdfHandler: arguments: $builderLocator: '@nextpdf.pdf_builder_locator'Для CodeIgniter реализуйте построитель как статический метод в App\PdfBuilders. Задача отклоняет любую ссылку на построитель вне этого пространства имён и любой выходной путь вне WRITEPATH/pdfs/.
<?php
declare(strict_types=1);
namespace App\PdfBuilders;
use NextPDF\Core\Document;
final class InvoiceBuilder{ /** @param array<string, mixed> $context */ public static function build(Document $document, array $context): Document { $invoiceId = (int) ($context['invoice_id'] ?? 0);
$document->addPage(); $document->cell(0, 10, "Invoice #{$invoiceId}");
return $document; }}Запустите воркер для каждого фреймворка.
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600php spark queue:work pdf-queueПерезапускайте воркеры Laravel и Symfony с ограниченным временем жизни (--limit / --memory-limit / --time-limit), чтобы возможная утечка памяти в зависимости не могла расти бесконечно.
Граничные случаи и подводные камни
Заголовок раздела «Граничные случаи и подводные камни»- Сохраняется именно возвращаемое значение построителя. В каждой интеграции воркер сохраняет документ, который возвращает построитель, а не экземпляр, изначально разрешённый для него. Всегда возвращайте настроенный документ из построителя.
- Проверка пути выполняется на воркере. Symfony проверяет выходной путь при создании и ещё раз при потреблении. CodeIgniter ограничивает путь каталогом
WRITEPATH/pdfs/и отклоняет пути с обходом каталогов и пути с совпадающим префиксом соседнего каталога. Путь, который был безопасным при постановке, но небезопасным при потреблении, всё равно отклоняется. - CodeIgniter помещает имя, а не класс. Если вы передадите
GeneratePdfJob::classв качестве имени задачи, очередь отклонит его при постановке в очередь. Вместо этого передавайте ключ изjobHandlers. - Колбэки Laravel необходимо передавать в статический вызов dispatch. Если вы создадите экземпляр задачи, а затем вызовете
$job->dispatch(...), этот вызов отбросит экземпляр и его колбэки. Передавайте колбэки вGeneratePdfJob::dispatch(...). - Безопасные для воркеров реестры. Реестр шрифтов — это заблокированный синглтон на время жизни процесса, а реестр изображений — кэш ограниченного размера. Документы создаются заново для каждой задачи. Не запрашивайте общий документ на воркере.
- Подписание в воркерах. Для подписанного вывода или вывода PDF/A в задаче очереди требуется коммерческая редакция NextPDF, установленная в среде воркера. Без неё сервис подписания разрешается в
null. Перед подписанием проверяйте его на null.
Производительность
Заголовок раздела «Производительность»Перенос генерации в задачу очереди убирает всё время сборки PDF из HTTP-запроса. Запрос завершается, как только задача поставлена в очередь. Реестры шрифтов и изображений распределяют затраты на инициализацию по всему времени жизни воркера, поэтому затраты на одну задачу ограничиваются построением документа и выводом содержимого. Подбирайте число одновременно выполняемых задач под ваш пул воркеров и предварительно заполняйте preload_fonts (Laravel, Symfony), чтобы прогрев шрифтов происходил один раз при запуске воркера, а не при первой задаче.
Замечания по безопасности
Заголовок раздела «Замечания по безопасности»- Если брокер доступен злоумышленнику, он может влиять на полезные нагрузки очереди, поэтому считайте выходной путь и ссылку на построитель в полезной нагрузке недоверенными. Интеграции обеспечивают это проверкой пути и, в CodeIgniter, списком разрешённых пространств имён построителей.
- Ограничьте права воркера в файловой системе ожидаемым каталогом вывода по принципу эшелонированной защиты. Если подделанный путь каким-то образом пройдёт проверку, он всё равно не сможет выйти за пределы каталога.
- В колбэке сбоя записывайте в журнал класс исключения и идентификатор корреляции, но никогда — сообщение или трассировку.
- Никогда не пишите пустой блок
catch. Каждый колбэк сбоя здесь ведёт журнал и сохраняет контекст.
Страница по безопасности и эксплуатации для каждой интеграции охватывает полную модель угроз очереди: проверку полезной нагрузки, списки разрешённых вызываемых объектов и ограничение пути.
Соответствие
Заголовок раздела «Соответствие»Это руководство не делает заявлений о соответствии нормативным стандартам. Каждый показанный вызов API — это проверенная публичная поверхность названной интеграции. Вариант с очередью полагается на гарантии привязки контейнера: новый документ при каждом разрешении и заблокированный реестр шрифтов. Страницы по использованию в продакшене, на которые есть ссылки в разделе “См. также”, документируют эти гарантии со своими ссылками на PSR. Эта страница сборника рецептов кратко повторяет сценарий использования и отсылает за ссылками к тем страницам.
См. также
Заголовок раздела «См. также»- Возврат сгенерированного PDF из контроллера — синхронный аналог.
- Использование Laravel в продакшене —
GeneratePdfJob, колбэки и таблица настроек очереди. - Использование Symfony в продакшене — безопасность воркеров Messenger и локатор построителей.
- Использование CodeIgniter в продакшене —
GeneratePdfJob,jobHandlersи ограничение пути.