Have 30-200 Employees? Make $50k-$500k selling your data for AI Training.

Learn More
Community Article
Community articles are authored by SitePoint Premium contributors. Content is screened before publication, and SitePoint reserves the right to moderate or remove articles that violate our guidelines. Views expressed are those of the authors and do not necessarily reflect those of SitePoint.

How to Build a Simple PDF Invoice Generator with PHP

Published in
PHP·
·Updated:

The AI briefing for Developers

Stay up to date with AI tools, model releases, and developer workflows that matter.

Weekly. Free. One click to leave.

Share this article

How to Build a Simple PDF Invoice Generator with PHP
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
  • Premium Results
  • Publish articles on SitePoint
  • Daily curated jobs
  • Learning Paths
  • Discounts to dev tools
Start Free Trial

7 Day Free Trial. Cancel Anytime.

Every freelancer and small dev shop eventually needs a way to turn an order or a project record into a proper PDF invoice. The naive approach, printing an HTML page to PDF from the browser, works until a client opens it on a different device and the layout breaks, or until you need to generate invoices automatically from a backend process with nobody sitting at a browser at all. This is a practical walkthrough of generating clean, consistent PDF invoices directly from PHP, without relying on a browser print dialog.

Picking a PDF Library

PHP has several mature options for generating PDFs directly, without going through HTML rendering at all. FPDF and its extended fork FPDI are lightweight and dependency free. TCPDF adds more built in support for tables, fonts, and encoding. Dompdf takes a different approach entirely, rendering HTML and CSS into a PDF, which is convenient if you already have an HTML invoice template and don't want to rebuild the layout using drawing commands.

SitePoint's own guide to generating PDFs with PHP covers the broader landscape of these libraries if you want a fuller comparison before picking one. For this walkthrough, FPDF is a reasonable default: no external dependencies beyond the library itself, and drawing an invoice layout by hand keeps the output predictable across PDF viewers, which matters more for financial documents than it might for other content.

Structuring the Invoice Data

Before touching PDF drawing code, define the invoice as a plain data structure. Keeping the data separate from the rendering logic means you can swap PDF libraries later, or add an HTML or CSV export, without touching how invoices are calculated.



class Invoice
{
    public string $invoiceNumber;
    public string $issueDate;
    public string $dueDate;
    public array $fromDetails;
    public array $toDetails;
    public array $lineItems = [];
    public float $taxRate = 0.0;

    public function addLineItem(string $description, int $quantity, float $unitPrice): void
    {
        $this->lineItems[] = [
            'description' => $description,
            'quantity' => $quantity,
            'unitPrice' => $unitPrice,
            'total' => $quantity * $unitPrice,
        ];
    }

    public function subtotal(): float
    {
        return array_sum(array_column($this->lineItems, 'total'));
    }

    public function taxAmount(): float
    {
        return $this->subtotal() * $this->taxRate;
    }

    public function total(): float
    {
        return $this->subtotal() + $this->taxAmount();
    }
}

This class does none of the PDF work. It just holds the data and does the arithmetic, which is worth testing on its own before you ever generate a PDF. A subtotal or tax calculation bug is much easier to catch in a unit test than by squinting at a rendered document.

Rendering the PDF

With the data structure in place, the rendering step becomes mostly a matter of positioning text and drawing a table.


require('fpdf/fpdf.php');

class InvoicePdf extends FPDF
{
    private Invoice $invoice;

    public function __construct(Invoice $invoice)
    {
        parent::__construct();
        $this->invoice = $invoice;
    }

    public function generate(): string
    {
        $this->AddPage();
        $this->SetFont('Arial', 'B', 18);
        $this->Cell(0, 12, 'INVOICE', 0, 1);

        $this->SetFont('Arial', '', 10);
        $this->Cell(0, 6, 'Invoice #: ' . $this->invoice->invoiceNumber, 0, 1);
        $this->Cell(0, 6, 'Issued: ' . $this->invoice->issueDate, 0, 1);
        $this->Cell(0, 6, 'Due: ' . $this->invoice->dueDate, 0, 1);
        $this->Ln(8);

        $this->SetFont('Arial', 'B', 10);
        $this->Cell(60, 8, 'From', 0, 0);
        $this->Cell(60, 8, 'Bill To', 0, 1);
        $this->SetFont('Arial', '', 10);
        $this->Cell(60, 6, $this->invoice->fromDetails['name'], 0, 0);
        $this->Cell(60, 6, $this->invoice->toDetails['name'], 0, 1);
        $this->Ln(10);

        $this->SetFont('Arial', 'B', 10);
        $this->Cell(90, 8, 'Description', 1, 0);
        $this->Cell(25, 8, 'Qty', 1, 0, 'C');
        $this->Cell(35, 8, 'Unit Price', 1, 0, 'R');
        $this->Cell(35, 8, 'Total', 1, 1, 'R');

        $this->SetFont('Arial', '', 10);
        foreach ($this->invoice->lineItems as $item) {
            $this->Cell(90, 8, $item['description'], 1, 0);
            $this->Cell(25, 8, (string) $item['quantity'], 1, 0, 'C');
            $this->Cell(35, 8, number_format($item['unitPrice'], 2), 1, 0, 'R');
            $this->Cell(35, 8, number_format($item['total'], 2), 1, 1, 'R');
        }

        $this->Ln(4);
        $this->Cell(150, 8, 'Subtotal', 0, 0, 'R');
        $this->Cell(35, 8, number_format($this->invoice->subtotal(), 2), 0, 1, 'R');
        $this->Cell(150, 8, 'Tax', 0, 0, 'R');
        $this->Cell(35, 8, number_format($this->invoice->taxAmount(), 2), 0, 1, 'R');
        $this->SetFont('Arial', 'B', 10);
        $this->Cell(150, 8, 'Total', 0, 0, 'R');
        $this->Cell(35, 8, number_format($this->invoice->total(), 2), 0, 1, 'R');

        return $this->Output('S');
    }
}

Calling $this->Output('S') rather than 'D' or 'I' returns the PDF as a string rather than sending it directly to the browser, which matters if you want to save it to disk, attach it to an email, or store it in cloud storage instead of streaming it straight to a response.

Zend Framework users have a slightly different but comparable path. SitePoint's older piece on generating invoices with Zend PDF walks through the same core idea using Zend's PDF component instead of FPDF, worth a look if that's already part of your stack.

Handling the Common Edge Cases

A few details separate a working prototype from something you'd actually trust in production.

Currency formatting. number_format alone doesn't handle currency symbols or locale specific separators. If you're invoicing across currencies, wrap formatting in a small helper that takes a currency code and applies the right symbol and decimal convention rather than hardcoding a dollar sign.

Long descriptions. The Cell method in FPDF truncates text that's wider than the cell. For line item descriptions that might run long, use MultiCell instead, and calculate row heights dynamically based on how many lines the description wraps to, so the table doesn't overlap itself.

Page breaks. An invoice with enough line items will eventually run past a single page. FPDF's AcceptPageBreak method lets you detect when you're near the bottom margin and either start a new page or shrink row height, rather than letting content silently run off the page.

Storage and retrieval. Once generated, decide whether invoices get regenerated on demand or stored as static files. Storing the generated PDF alongside a record of the invoice data it came from makes it possible to reproduce the exact document later, even if your rendering code changes, which matters for anything that might need to match what a client was actually sent.

When Not to Build This Yourself

This approach makes sense when invoices need to be generated automatically as part of a larger system, tied to order records, subscription billing, or a project management tool you're already building. If you just need to send the occasional one off invoice and don't need programmatic generation, a hosted tool like Designs Valley's invoice generator handles the same layout and calculation work without any setup, which is often the faster path for a single freelancer who doesn't need this wired into a backend.

Frequently Asked Questions

Which PHP PDF library should I use for invoices?

FPDF or TCPDF if you're comfortable positioning content with drawing commands, Dompdf if you'd rather write the invoice as HTML and CSS and let the library handle the conversion. Zend PDF is a reasonable choice if you're already in the Zend ecosystem.

How do I handle multiple currencies in generated invoices?

Store the currency code alongside each invoice record and format amounts using a locale aware helper rather than hardcoding a symbol, since number formatting conventions differ by currency and region.

What happens if an invoice has too many line items for one page?

Use FPDF's page break detection to start a new page automatically rather than letting the table overflow, and repeat the table header on the new page so the columns stay readable.

Should invoices be generated fresh every time or stored as files?

Store the generated PDF alongside the invoice data used to create it. That way you have an exact record of what was sent, even if you later change your template or rendering logic.

Summary

Generating invoices directly from PHP, rather than relying on a browser print dialog, gives you consistent, automatable output that fits naturally into a billing or order system. Separating the invoice data structure from the PDF rendering logic keeps the arithmetic testable and the output format swappable, and handling the edge cases around currency formatting, long descriptions, and page breaks is what separates a working prototype from something reliable enough to send to a real client.

© 2000 – 2026 SitePoint Pty. Ltd.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.