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 inPHP·
August 24, 2026
·Updated:August 24, 2026
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.
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
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.
<?phpclassInvoice{publicstring$invoiceNumber;publicstring$issueDate;publicstring$dueDate;publicarray$fromDetails;publicarray$toDetails;publicarray$lineItems=[];publicfloat$taxRate=0.0;publicfunctionaddLineItem(string$description,int$quantity,float$unitPrice):void{$this->lineItems[]=['description'=>$description,'quantity'=>$quantity,'unitPrice'=>$unitPrice,'total'=>$quantity*$unitPrice,];}publicfunctionsubtotal():float{returnarray_sum(array_column($this->lineItems,'total'));}publicfunctiontaxAmount():float{return$this->subtotal()*$this->taxRate;}publicfunctiontotal():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.
<?phprequire('fpdf/fpdf.php');classInvoicePdfextendsFPDF{privateInvoice$invoice;publicfunction__construct(Invoice$invoice){parent::__construct();$this->invoice=$invoice;}publicfunctiongenerate():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->lineItemsas$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.


