Skip to content

PDF Generation

Office App has built-in PDF generation using TCPDF. You can define multiple PDF templates per model, preview them in the browser, download them, or auto-attach them to a record's file field.

Step 1: Define PDF Types

Override the static getPdfs() method to declare available PDFs:

use System\Pdf\PdfDefinition;

public static function getPdfs(): array
{
    return [
        new PdfDefinition('invoice', 'Invoice'),
        new PdfDefinition('summary', 'Summary Report'),
        (new PdfDefinition('pass', 'Access Pass'))->attachTo('document'),  // (1)
    ];
}
  1. attachTo('document') automatically saves the generated PDF into the document PdfProperty field when the user clicks "Attach".

Each definition creates a button in the record's toolbar under a "PDF" group with options to Preview, Download, and Attach (if configured).

Step 2: Implement the Builder

Override getPdf() to dispatch to your builder methods:

use System\Pdf\PdfContext;
use System\Pdf\PdfDocument;

public function getPdf(string $key, PdfContext $ctx): PdfDocument
{
    return match ($key) {
        'invoice' => $this->buildInvoicePdf(),
        'summary' => $this->buildSummaryPdf(),
        'pass'    => $this->buildPassPdf(),
        default   => throw new \InvalidArgumentException("Unknown PDF: $key"),
    };
}

Step 3: Build the PDF

A builder method returns a PdfDocument with a closure that receives a Pdf instance (extends TCPDF):

use System\Pdf\Pdf;
use System\Pdf\PdfDocument;
use System\UI\Lang;

private function buildInvoicePdf(): PdfDocument
{
    $record = $this;

    return (new PdfDocument(function (Pdf $pdf) use ($record) {

        // Heading
        $pdf->heading(Lang::t('Invoice'));

        // Key-value table
        $pdf->keyValueTable([
            [Lang::t('Title'),     $record->title      ?? '—'],
            [Lang::t('Reference'), $record->reference   ?? '—'],
            [Lang::t('Value'),     $record->value       ?? '—'],
            [Lang::t('Date'),      $record->start_date  ?? '—'],
            [Lang::t('Status'),    $record->_state      ?? '—'],
        ]);

        // Spacer
        $pdf->spacer(6);

        // Body text
        $pdf->body(
            Lang::t('Thank you for your business.'),
            11  // font size
        );

    }))
    ->title(Lang::t('Invoice') . ': ' . ($record->title ?? ''))
    ->orientation('P')     // 'P' = portrait, 'L' = landscape
    ->pageSize('A4');      // 'A4', 'Letter', etc.
}

PDF Helper Methods

The Pdf class (extends TCPDF) provides these convenience methods:

heading(string $text, int $size = 14)

Prints a bold heading.

$pdf->heading('Contract Summary');
$pdf->heading('Section Title', 12);   // smaller heading

body(string $text, int $size = 10)

Prints body text with automatic line wrapping.

$pdf->body('This is a paragraph of text.');
$pdf->body($longText, 9);   // smaller font

keyValueTable(array $rows)

Prints a two-column table. Each row is [label, value].

1
2
3
4
5
$pdf->keyValueTable([
    ['Name',    $this->name],
    ['Email',   $this->email],
    ['Status',  $this->status],
]);

spacer(int $mm)

Adds vertical whitespace.

$pdf->spacer(10);   // 10mm gap

imageFromDataUri(string $dataUri, float $w, float $h, string $type)

Embed an image from a data URI (useful for QR codes).

1
2
3
$qr = new QRCodeProperty('qr');
$dataUri = $qr->getDataUri(url: $recordUrl, size: 80);
$pdf->imageFromDataUri($dataUri, 25, 25, 'PNG');

TCPDF Native Methods

Since Pdf extends TCPDF, you have access to all TCPDF methods:

1
2
3
4
5
6
$pdf->SetFont('dejavusans', '', 10);
$pdf->Cell(0, 8, 'Cell text', 1, 1);
$pdf->MultiCell(0, 5, 'Multi-line text', 1);
$pdf->Ln(5);
$pdf->SetFillColor(240, 240, 240);
$pdf->write1DBarcode('ABC-123', 'C128', '', '', 60, 15);

PdfDocument Options

1
2
3
4
(new PdfDocument($closure))
    ->title('Document Title')          // shown in browser tab
    ->orientation('P')                 // 'P' portrait, 'L' landscape
    ->pageSize('A4')                   // 'A4', 'Letter', 'Legal', etc.

Complete Example: Contract Summary with QR Code

use System\Pdf\Pdf;
use System\Pdf\PdfContext;
use System\Pdf\PdfDefinition;
use System\Pdf\PdfDocument;
use System\QRCodeProperty;
use System\UI\Lang;

class Contract extends DefaultModel
{
    public static function getPdfs(): array
    {
        return [
            (new PdfDefinition('summary', 'Contract Summary'))
                ->attachTo('document'),
            new PdfDefinition('cover_letter', 'Cover Letter'),
        ];
    }

    public function getPdf(string $key, PdfContext $ctx): PdfDocument
    {
        return match ($key) {
            'summary'      => $this->buildSummaryPdf(),
            'cover_letter' => $this->buildCoverLetterPdf(),
            default        => throw new \InvalidArgumentException("Unknown PDF: $key"),
        };
    }

    private function buildSummaryPdf(): PdfDocument
    {
        $contract = $this;
        return (new PdfDocument(function (Pdf $pdf) use ($contract) {

            $pdf->heading(Lang::t('Contract Summary'));

            $pdf->keyValueTable([
                [Lang::t('Title'),     $contract->title      ?? '—'],
                [Lang::t('Reference'), $contract->reference   ?? '—'],
                [Lang::t('Value'),     $contract->value       ?? '—'],
                [Lang::t('Start'),     $contract->start_date  ?? '—'],
                [Lang::t('End'),       $contract->end_date    ?? '—'],
                [Lang::t('Status'),    $contract->_state      ?? '—'],
            ]);

            $pdf->spacer(6);

            // QR code linking to this record
            $url = rtrim(APP_URL, '/') . '?model=contract&id=' . $contract->getId();
            $qr = new QRCodeProperty('qr');
            $dataUri = $qr->getDataUri(url: $url, size: 80);

            if ($dataUri !== '') {
                $pdf->body(Lang::t('Scan to open record:'), 8);
                $pdf->imageFromDataUri($dataUri, 25, 25, 'PNG');
            }

        }))
        ->title(Lang::t('Contract Summary') . ': ' . ($this->title ?? ''))
        ->orientation('P')
        ->pageSize('A4');
    }

    private function buildCoverLetterPdf(): PdfDocument
    {
        $contract = $this;
        return (new PdfDocument(function (Pdf $pdf) use ($contract) {

            $pdf->heading(Lang::t('Cover Letter'), 16);
            $pdf->body(date('d.m.Y'));
            $pdf->spacer(8);

            $pdf->body(
                Lang::t('Dear Sir or Madam,') . "\n\n" .
                Lang::t('Please find enclosed the contract') .
                ' "' . ($contract->title ?? '') . '".' . "\n\n" .
                Lang::t('Yours sincerely,'),
                11
            );

        }))
        ->title(Lang::t('Cover Letter') . ': ' . ($this->title ?? ''))
        ->orientation('P')
        ->pageSize('A4');
    }
}