When a database contains more records than one PDF page can hold, the report must continue onto additional pages. With FPDF we can manage this in two different ways.
Database records
|
v
+-----------------------------+
| Strategy 1 |
| Fixed records per page |
+-----------------------------+
|
OR
|
+-----------------------------+
| Strategy 2 |
| Check available PDF space |
+-----------------------------+
|
v
Multi-page PDF table
The first method is useful when every row has a predictable height. The second method is more flexible because it checks the current vertical position before adding the next record.
Show Table of ContentsBoth approaches produce multiple PDF pages, but they decide when to create a new page differently.
In this method we decide how many database records should appear on each page. The example uses 10 records per page.
First count the records that will be included in the report.
$sql_total="SELECT COUNT(*) FROM student";
$total_records=(int)$dbo->query($sql_total)->fetchColumn();
fetchColumn() is enough here because the COUNT query returns one value.
Set the records per page and divide the total record count by that value.
$records_per_page=10;
$total_pages=(int)ceil(
$total_records/$records_per_page
);
For example:
35 records / 10 per page = 4 PDF pages
The final page contains the remaining records.
The page number determines the SQL offset.
$offset=$page*$records_per_page;
Instead of inserting those values directly into the SQL string, bind them as integers:
$sql="SELECT id,name,class,mark,gender
FROM student
ORDER BY id
LIMIT :limit OFFSET :offset";
$stmt=$dbo->prepare($sql);
$stmt->bindValue(':limit',$records_per_page,PDO::PARAM_INT);
$stmt->bindValue(':offset',$offset,PDO::PARAM_INT);
$stmt->execute();
$rows=$stmt->fetchAll(PDO::FETCH_ASSOC);
The explicit ORDER BY id is important because pagination should use a predictable record order.
<?php
require 'config.php';
require 'fpdf.php';
function addTableHeader($pdf,$width_cell)
{
$pdf->SetFont('Arial','B',14);
$pdf->SetFillColor(193,229,252);
$pdf->Cell(
$width_cell[0],
10,
'ID',
1,
0,
'C',
true
);
$pdf->Cell(
$width_cell[1],
10,
'NAME',
1,
0,
'C',
true
);
$pdf->Cell(
$width_cell[2],
10,
'CLASS',
1,
0,
'C',
true
);
$pdf->Cell(
$width_cell[3],
10,
'MARK',
1,
0,
'C',
true
);
$pdf->Cell(
$width_cell[4],
10,
'GENDER',
1,
1,
'C',
true
);
}
try{
$sql_total="SELECT COUNT(*) FROM student";
$total_records=(int)$dbo->query($sql_total)->fetchColumn();
$records_per_page=10;
$total_pages=$total_records>0
? (int)ceil($total_records/$records_per_page)
: 1;
$pdf=new FPDF();
$pdf->SetAutoPageBreak(true,15);
$width_cell=[20,50,40,40,40];
$sql="SELECT id,name,class,mark,gender
FROM student
ORDER BY id
LIMIT :limit OFFSET :offset";
$stmt=$dbo->prepare($sql);
for($page=0;$page<$total_pages;$page++){
$pdf->AddPage();
addTableHeader($pdf,$width_cell);
$offset=$page*$records_per_page;
$stmt->bindValue(
':limit',
$records_per_page,
PDO::PARAM_INT
);
$stmt->bindValue(
':offset',
$offset,
PDO::PARAM_INT
);
$stmt->execute();
$rows=$stmt->fetchAll(PDO::FETCH_ASSOC);
$pdf->SetFont('Arial','',12);
$pdf->SetFillColor(235,236,236);
$fill=false;
foreach($rows as $row){
$pdf->Cell(
$width_cell[0],
10,
(string)$row['id'],
1,
0,
'C',
$fill
);
$pdf->Cell(
$width_cell[1],
10,
(string)$row['name'],
1,
0,
'L',
$fill
);
$pdf->Cell(
$width_cell[2],
10,
(string)$row['class'],
1,
0,
'C',
$fill
);
$pdf->Cell(
$width_cell[3],
10,
(string)$row['mark'],
1,
0,
'C',
$fill
);
$pdf->Cell(
$width_cell[4],
10,
(string)$row['gender'],
1,
1,
'C',
$fill
);
$fill=!$fill;
}
if(!$rows && $page===0){
$pdf->SetFont('Arial','',12);
$pdf->Cell(
190,
10,
'No student records found.',
1,
1,
'C'
);
}
}
$pdf->Output(
'I',
'student-multiple-pages.pdf'
);
}catch(PDOException $e){
error_log($e->getMessage());
exit('Unable to generate the PDF report.');
}
DEMO: Fixed Number of Records per PDF Page
The fixed-record method works well when every database row uses the same height. A more flexible approach is to check whether enough vertical space remains before drawing the next row.
Current Y position
+
Next row height
|
v
Will row fit?
/ \
Yes No
| |
Draw row AddPage()
|
v
Repeat header
This avoids having to decide in advance how many records must fit on each page.
The original example used a hard-coded value of 280. A more adaptable approach uses the current page height.
$row_height=10;
$bottom_margin=15;
$page_height=$pdf->GetPageHeight();
if(
$pdf->GetY()+$row_height
>
$page_height-$bottom_margin
){
$pdf->AddPage();
}
This is better than assuming one specific page height because the condition is based on the actual PDF page dimensions.
When a new page is created, the table headings should appear again before the next record.
if(
$pdf->GetY()+$row_height
>
$page_height-$bottom_margin
){
$pdf->AddPage();
addTableHeader($pdf,$width_cell);
}
Using one helper function keeps the header formatting identical on every page.
<?php
require 'config.php';
require 'fpdf.php';
function addTableHeader($pdf,$width_cell)
{
$pdf->SetFont('Arial','B',14);
$pdf->SetFillColor(193,229,252);
$headers=['ID','NAME','CLASS','MARK','GENDER'];
foreach($headers as $index=>$header){
$line=$index===count($headers)-1
? 1
: 0;
$pdf->Cell(
$width_cell[$index],
10,
$header,
1,
$line,
'C',
true
);
}
}
try{
$sql="SELECT id,name,class,mark,gender
FROM student
ORDER BY id";
$stmt=$dbo->prepare($sql);
$stmt->execute();
$pdf=new FPDF();
$pdf->SetAutoPageBreak(false);
$pdf->AddPage();
$width_cell=[20,50,40,40,40];
$row_height=10;
$bottom_margin=15;
$page_height=$pdf->GetPageHeight();
addTableHeader($pdf,$width_cell);
$pdf->SetFont('Arial','',12);
$pdf->SetFillColor(235,236,236);
$fill=false;
$has_rows=false;
while($row=$stmt->fetch(PDO::FETCH_ASSOC)){
$has_rows=true;
if(
$pdf->GetY()+$row_height
>
$page_height-$bottom_margin
){
$pdf->AddPage();
addTableHeader($pdf,$width_cell);
$pdf->SetFont(
'Arial',
'',
12
);
$pdf->SetFillColor(
235,
236,
236
);
}
$pdf->Cell(
$width_cell[0],
$row_height,
(string)$row['id'],
1,
0,
'C',
$fill
);
$pdf->Cell(
$width_cell[1],
$row_height,
(string)$row['name'],
1,
0,
'L',
$fill
);
$pdf->Cell(
$width_cell[2],
$row_height,
(string)$row['class'],
1,
0,
'C',
$fill
);
$pdf->Cell(
$width_cell[3],
$row_height,
(string)$row['mark'],
1,
0,
'C',
$fill
);
$pdf->Cell(
$width_cell[4],
$row_height,
(string)$row['gender'],
1,
1,
'C',
$fill
);
$fill=!$fill;
}
if(!$has_rows){
$pdf->SetFont('Arial','',12);
$pdf->Cell(
190,
10,
'No student records found.',
1,
1,
'C'
);
}
$pdf->Output(
'I',
'student-dynamic-pages.pdf'
);
}catch(PDOException $e){
error_log($e->getMessage());
exit('Unable to generate the PDF report.');
}
DEMO: Multi-Page PDF based on Available Space
Cell() rows used in this tutorial, both methods work well. If table rows later contain wrapped MultiCell() content, the required row height must also be calculated before deciding whether the row fits on the current page.The main database PDF tutorial contains the downloadable project with the sample student table, MySQL version and SQLite-supported version.
Download Database-to-PDF ProjectUse an explicit ORDER BY clause when paginating database records. Without a defined order, rows should not be assumed to appear in the same sequence every time.
Bind both values as PDO::PARAM_INT. They are numeric SQL values and should not be treated as quoted strings.
Call the table-header function immediately after every AddPage().
Reserve a bottom margin and check the current Y coordinate plus the next row height before drawing the row.
Make sure $row_height matches the actual height used by the row Cells, and adjust the bottom margin when the document has a footer or additional page content.
A fixed-height Cell() does not wrap text. If a table uses MultiCell(), calculate the full rendered row height before deciding whether the next row fits on the page.
Fetch the database records and call AddPage() whenever a new PDF page is required. Repeat the table header after each page is added.
Count the total records, set a records-per-page value, calculate the required pages with ceil(), and use LIMIT and OFFSET to fetch the records for each page.
Read the current Y position with GetY(), add the next row height and compare the result with the page height minus the reserved bottom margin.
Place the heading Cells inside a reusable function and call that function immediately after every AddPage().
LIMIT and OFFSET expect numeric values. Binding them with PDO::PARAM_INT sends the values with the appropriate type.
Fixed pagination is convenient when every row has a known height. Available-space checking is more flexible when the amount of content or page layout can vary.
Yes, but the complete rendered row height must be calculated before deciding whether the row fits because MultiCell can wrap text over several lines.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.