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

Возврат сгенерированного PDF из контроллера

Сгенерируйте PDF-файл в действии контроллера и верните его как HTTP-ответ. Каждая интеграция с фреймворком включает помощник PdfResponse: он создаёт объект ответа, задаёт Content-Type: application/pdf, добавляет заголовки безопасности и очищает имя файла. В этом руководстве рассматриваются три режима доставки для Laravel, Symfony и CodeIgniter 4: встроенный предварительный просмотр, загрузка файла и потоковая передача.

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

  • Ядро NextPDF установлено.
  • Установлена одна интеграция с фреймворком, и её поставщик служб, бандл или служба обнаружены. Перед началом работы проверьте это на странице установки для вашего фреймворка.
  • Потоковый режим не требует дополнительных пакетов. Каждая интеграция включает потоковый вариант наряду с буферизованным вариантом.

Это практическое руководство. Предполагается, что вы уже знаете, как направить запрос к контроллеру в своём фреймворке. Чтобы получить первый рабочий пример для каждого фреймворка, см. соответствующее краткое руководство в разделе “См. также”.

Установите интеграцию для своего фреймворка. Выполните одну из команд ниже.

Окно терминала
composer require nextpdf/laravel
Окно терминала
composer require nextpdf/symfony
Окно терминала
composer require nextpdf/codeigniter

Для Laravel опубликуйте конфигурацию после установки.

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

Symfony регистрирует бандл через Flex, а CodeIgniter обнаруживает службу автоматически. Прежде чем продолжить, убедитесь на странице установки вашего фреймворка, что обнаружение прошло успешно.

Каждая интеграция с фреймворком следует одной и той же трёхчастной схеме: вы получаете новый документ, записываете в него содержимое и передаёте документ фабрике PdfResponse, которая возвращает HTTP-ответ. API документа (addPage(), cell(), setFont()) относится к основному движку и одинаков во всех фреймворках. Фабрики ответов различаются только возвращаемым классом ответа, потому что у каждого фреймворка свой тип HTTP-ответа.

PdfResponse предлагает три режима доставки. Встроенный режим задаёт заголовок Content-Disposition: inline, поэтому браузер показывает PDF во вкладке просмотра. Загрузка задаёт Content-Disposition: attachment, поэтому браузер сохраняет файл. Потоковая передача выдаёт тело PDF фиксированными порциями вместо буферизации всего документа в памяти. Выбирайте её для больших документов, когда пиковое потребление памяти важнее известного значения Content-Length.

Получите документ через обычный путь разрешения вашего фреймворка:

  • Laravel — разрешите NextPDF\Contracts\DocumentFactoryInterface из контейнера с помощью app(...) и вызовите create(), который возвращает новый NextPDF\Core\Document — конкретный тип, принимаемый фабриками PdfResponse.
  • Symfony — внедрите NextPDF\Symfony\Service\PdfFactory и вызовите create(), который возвращает новый NextPDF\Core\Document с уже применёнными настройками документа по умолчанию.
  • CodeIgniter 4 — разрешите библиотеку Pdf через Services::pdf() (или помощник pdf()) либо получите чистый документ через pdf_document().
ЗадачаLaravelSymfonyCodeIgniter 4
Новый документapp(DocumentFactoryInterface::class)->create()PdfFactory::create()pdf_document() / Services::pdf()->document()
Встроенный ответPdfResponse::inline($doc, $name)PdfResponse::inline($doc, $name)$pdf->inline($name) / PdfResponse::inline($doc, $name)
Ответ для загрузкиPdfResponse::download($doc, $name)PdfResponse::download($doc, $name)$pdf->download($name) / PdfResponse::download($doc, $name)
Потоковый встроенныйPdfResponse::streamInline($doc, $name)PdfResponse::streamInline($doc, $name)PdfResponse::streamInline($doc, $name)
Потоковая загрузкаPdfResponse::streamDownload($doc, $name)PdfResponse::streamDownload($doc, $name)PdfResponse::streamDownload($doc, $name)
Возвращаемый типIlluminate\Http\Response (потоковый: StreamedResponse)Symfony\Component\HttpFoundation\Response (потоковый: StreamedResponse)CodeIgniter\HTTP\DownloadResponse

В Laravel PdfResponse находится по адресу NextPDF\Laravel\Http\PdfResponse, в Symfony — NextPDF\Symfony\Http\PdfResponse, а в CodeIgniter — NextPDF\CodeIgniter\Http\PdfResponse. Страница безопасности и эксплуатации каждой интеграции описывает полное поведение ответа в этом пакете: набор заголовков, правила disposition и очистку имени файла. Ссылки на эти страницы приведены в разделе “См. также”.

Ниже приведено минимальное действие загрузки для каждого фреймворка. Вызовы документа используют один и тот же API ядра. Меняется только структура контроллера.

Laravel: app/Http/Controllers/ReportController.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Response;
use NextPDF\Contracts\DocumentFactoryInterface;
use NextPDF\Laravel\Http\PdfResponse;
final class ReportController extends Controller
{
public function download(): Response
{
$document = app(DocumentFactoryInterface::class)->create();
$document->addPage();
$document->cell(0, 10, 'Monthly report', newLine: true);
return PdfResponse::download($document, 'report.pdf');
}
}
Symfony: src/Controller/ReportController.php
<?php
declare(strict_types=1);
namespace App\Controller;
use NextPDF\Symfony\Http\PdfResponse;
use NextPDF\Symfony\Service\PdfFactory;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class ReportController
{
#[Route('/report', name: 'report_pdf')]
public function download(PdfFactory $pdf): Response
{
$document = $pdf->create();
$document->addPage();
$document->cell(0, 10, 'Monthly report', newLine: true);
return PdfResponse::download($document, 'report.pdf');
}
}
CodeIgniter 4: app/Controllers/ReportController.php
<?php
declare(strict_types=1);
namespace App\Controllers;
use CodeIgniter\HTTP\DownloadResponse;
use NextPDF\CodeIgniter\Config\Services;
final class ReportController extends BaseController
{
public function download(): DownloadResponse
{
$pdf = Services::pdf();
$pdf->document()->addPage();
$pdf->document()->cell(0, 10, 'Monthly report');
return $pdf->download('report.pdf');
}
}

Чтобы открыть предварительный просмотр в браузере вместо загрузки, замените вызов download(...) на inline(...) в Laravel и Symfony либо на $pdf->inline('report.pdf') в CodeIgniter. Значение disposition меняется на inline, а все остальные заголовки остаются прежними.

Действие для продакшена внедряет свои зависимости, перехватывает наиболее конкретное исключение, задокументированное интеграцией, записывает класс сбоя в журнал без утечки трассировки и возвращает явную ошибку HTTP. В примере ниже используется внедрение через конструктор Laravel. Эквиваленты для Symfony и CodeIgniter следуют той же схеме и приведены на странице эксплуатации в продакшене каждой интеграции.

Laravel: app/Http/Controllers/InvoiceController.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Response;
use NextPDF\Contracts\DocumentFactoryInterface;
use NextPDF\Laravel\Http\PdfResponse;
use Psr\Log\LoggerInterface;
use Throwable;
final class InvoiceController extends Controller
{
public function __construct(
private readonly DocumentFactoryInterface $documents,
private readonly LoggerInterface $logger,
) {}
public function show(int $invoiceId): Response
{
try {
$document = $this->documents->create();
$document->addPage();
$document->cell(0, 10, "Invoice #{$invoiceId}", newLine: true);
return PdfResponse::download(
$document,
"invoice-{$invoiceId}.pdf",
);
} catch (Throwable $exception) {
// Log the exception class, never the message or a stack trace,
// so internal detail does not leak into the log sink.
$this->logger->error('Invoice PDF generation failed', [
'invoice_id' => $invoiceId,
'exception' => $exception::class,
]);
return new Response('Could not generate the invoice PDF.', 500);
}
}
}

Внедрите DocumentFactoryInterface и вызывайте create() в каждом действии. Он возвращает новый NextPDF\Core\Document — конкретный тип, который принимают фабрики PdfResponse для Laravel. Разрешение нового документа для каждого запроса сохраняет возможность подмены фабрики в тестах. Не используйте повторно один экземпляр контроллера для двух логически не связанных документов в рамках одного длительно работающего рабочего процесса.

Для очень больших документов замените буферизованную фабрику потоковой, чтобы ограничить пиковое потребление памяти. Потоковый вариант возвращает StreamedResponse (Laravel и Symfony) и выдаёт тело фиксированными порциями. Он намеренно не указывает Content-Length, поэтому индикаторы хода загрузки и прокси, зависящие от длины тела, не видят известного размера. Для небольших ответов, чувствительных к задержке, предпочитайте буферизованные download() / inline().

Laravel: streamed download for a large report
$document = $this->documents->create();
// ... emit content onto $document ...
return PdfResponse::streamDownload($document, 'annual-report.pdf');
  • Новый документ на каждый вызов. Во всех трёх интеграциях документ создаётся фабрикой заново при каждом разрешении. Не кэшируйте разрешённый документ между логическими документами или между запросами в длительно работающем рабочем процессе. Иначе старое состояние содержимого перейдёт дальше.
  • Пустое имя файла. Пустое имя файла, переданное фабрике PdfResponse, заменяется именем по умолчанию (document.pdf), а не создаёт пустое значение disposition. Передавайте явное, осмысленное имя файла.
  • Имена файлов вне ASCII. Ответ Laravel автоматически добавляет параметр RFC 5987 filename*= для имён вне ASCII, а для имён ASCII используется обычный параметр. Не кодируйте имя файла вручную.
  • Потоковые ответы за буферизующим прокси. Прокси, который буферизует всё тело, сводит на нет выигрыш потоковой передачи по памяти. Настройте прокси на потоковую передачу PDF-ответов либо используйте на этом маршруте буферизованный ответ.
  • Потоковый обратный вызов Symfony. Потоковый вариант Symfony возвращает StreamedResponse, обратный вызов которого сбрасывает вывод. Не записывайте в тело ответа самостоятельно после того, как вы его вернули.

Синхронная генерация внутри контроллера блокирует запрос на всё время сборки PDF. Одностраничный документ обычно укладывается в типичный бюджет запроса. Для многостраничного или пакетного вывода вынесите генерацию из потока запроса в задание очереди — см. “Генерация PDF в задании очереди”. Потоковые варианты снижают пиковое потребление памяти для больших документов ценой неизвестного Content-Length. Выбирайте их, когда ограничением является память, а индикатор прогресса не требуется.

  • Фабрики PdfResponse применяют фиксированный набор заголовков, усиливающих безопасность ответа, и очищают имя файла загрузки в каждой интеграции. Не добавляйте эти заголовки самостоятельно.
  • Никогда не подставляйте непроверенный пользовательский ввод напрямую в имя файла, которое вы передаёте фабрике. Передавайте значение, которое вы контролируете, и позвольте фабрике очистить его как второй уровень защиты.
  • В блоке catch регистрируйте класс исключения и идентификатор корреляции, а не сообщение исключения или трассировку. Необработанная трассировка в приёмнике журналов — это утечка информации.
  • Никогда не пишите пустой блок catch. Каждый пример здесь регистрирует и возвращает определённый ответ об ошибке.

Страница безопасности и эксплуатации каждой интеграции описывает её модель угроз: набор заголовков, правила очистки имени файла и время жизни привязки документа.

Это руководство не делает нормативных заявлений о соответствии стандартам. Каждый показанный вызов API — это проверенная публичная поверхность указанной интеграции, сверенная со страницами быстрого старта и эксплуатации в продакшене каждого пакета. Страницы эксплуатации в продакшене, ссылки на которые приведены в разделе “См. также”, описывают семантику заголовков и поведение привязки в контейнере, на которые опираются интеграции, вместе с соответствующими ссылками на PSR. Эта страница руководства повторяет порядок использования, а нормативные ссылки оставляет этим страницам.