Skip to content

Generate PDFs in Winter Boot

Invoices, receipts, and reports usually leave the system as PDFs. The highly recommended pure-PHP library for this is Dompdf (dompdf/dompdf): it renders HTML/CSS to PDF with no binary dependencies, which makes HTML templates do double duty for email and print. This guide wires it as beans — an Options bean, a PdfService, a template, and a download endpoint.

Terminal window
composer require dompdf/dompdf

No module or application.yml entry is needed — the few knobs Dompdf has live in the Options bean below.

Four files. The controller streams the bytes with application/pdf and a download filename; the service renders HTML (shared template style with the Email guide); the config locks down fonts, temp files, and remote access. Switch between them:

The download endpoint — start here. PDF bytes go out as a raw string body with MediaType::APPLICATION_PDF; the filename is built from the integer id only, never from user input.

<?php
declare(strict_types=1);
namespace dev\example\rest;
use dev\example\service\PdfService;
use dev\winterframework\stereotype\Autowired;
use dev\winterframework\stereotype\RestController;
use dev\winterframework\stereotype\web\GetMapping;
use dev\winterframework\stereotype\web\PathVariable;
use dev\winterframework\stereotype\web\RequestMapping;
use dev\winterframework\web\http\ResponseEntity;
use dev\winterframework\web\MediaType;
#[RestController]
#[RequestMapping(path: 'invoices')]
class InvoiceController
{
#[Autowired]
protected PdfService $pdfs;
#[GetMapping(path: '{id}/pdf')]
public function downloadInvoice(#[PathVariable(name: 'id')] int $id): ResponseEntity
{
$pdf = $this->pdfs->renderInvoice($id);
return ResponseEntity::ok($pdf)
->withContentType(MediaType::APPLICATION_PDF)
->withContentLength(strlen($pdf))
->withHeader('Content-Disposition', 'attachment; filename="invoice-' . $id . '.pdf"');
}
}

Add the template directory to application.yml (with EnvPropertySource registered — see Configuration):

config/application.yml — additions
pdf:
templates: "templates/pdf"

One invoice renders in milliseconds — keep it synchronous in the request. For bulk work (month-end statements for ten thousand customers), generate in the background: an #[Async] method (needs #[EnableAsync] — see the Email guide) that renders and stores each PDF (local disk, S3 via the S3 module), or a scheduled job draining a queue. Never render bulk PDFs inside request handlers — Dompdf is CPU- and memory-hungry on large tables, and one big export will starve the worker pool.

  • Guard the endpoint. An invoice URL is a data leak shaped like a feature. Put #[RequirePermission](/winter-boot/howto/rbac) (or [#[RequireAbac]](/winter-boot/howto/abac) with an ownership rule) on the download method, and return 404— not403` — for invoices outside the caller’s scope, so ids cannot be probed.
  • Confine file access. If templates reference local images, also $options->set('chroot', $yourBaseDir) so a template path can never escape the intended directory.
  • Fonts. DejaVu ships with Dompdf and covers Latin, Cyrillic, Greek, and much more. For CJK or custom brand fonts, register them once with load_font.php and reference the family name in CSS.
  • Cache static documents. A regenerated-identical PDF (terms of service, fee schedules) can sit behind #[Cacheable] like any other bean result — see Caching. Per-customer documents must not be shared across cache keys.
  • Test the bytes. Assert the response starts with %PDF- and has a sane length — that catches template fatals and empty renders without parsing PDF.
  1. Seed an invoice row and hit GET /invoices/1/pdf.
  2. Expect 200, Content-Type: application/pdf, and a download prompt for invoice-1.pdf.
  3. Save the body and check the first five bytes are %PDF-.
  4. Request a missing id → the query throws and the error controller answers (never a half-written PDF).
  • Email — the same templates rendered for mail, plus async delivery.
  • RBAC / ABAC — guarding the download endpoint.
  • Caching — caching reusable documents.
  • REST controllers — response shapes and binary bodies.