Working with fillable pdf forms in php almost always starts the same way: a bank or government agency hands you a fixed PDF template with text boxes and checkboxes already drawn on it, and you have to populate it for thousands of records. The trick is to stop thinking about drawing on the PDF and start thinking about its AcroForm layer. The template already contains named form fields, so filling it means mapping each field name to a value, writing the result, then flattening so the output cannot be edited. Redrawing the page with FPDF or a fresh template is the wrong move, and it will never line up pixel-for-pixel with the official form.
What is an AcroForm and why does it change the approach?
An AcroForm is the interactive form layer Adobe defined inside the PDF spec. The page you see is static content; sitting on top of it is a dictionary of named fields, each with a type and an export value. Four field types cover almost everything you will touch in practice:
- Text fields — free text, the simplest case. You assign a string.
- Checkboxes — they have an 'on' export value (often 'Yes', '1', or something form-specific) and an 'Off' state. You must send the exact on-value, not a boolean.
- Radio button groups — one field name, several widgets, each widget with its own export value. You set the group to the chosen widget's value.
- Choice fields — dropdowns and list boxes, where the value must be one of the defined options.
Because the layout, fonts, and positions are baked into the template, you never touch coordinates. You only supply values keyed by field name. That is the entire mental model, and it is why this beats the redraw-it-yourself approach I weighed against other libraries in parsing and generating PDFs in PHP.
How do I find the exact field names in a PDF?
You cannot guess field names, and you cannot guess checkbox export values — they are whatever the form's author typed. The fastest way to read them is pdftk's dump_data_fields. Run it against the template once and keep the output as your field map reference.
# Install on Debian/Ubuntu
sudo apt-get install -y pdftk-java
# Dump every field name, type, and allowed value
pdftk loan-application.pdf dump_data_fields
# Pipe to a file you can search
pdftk loan-application.pdf dump_data_fields > fields.txtEach field comes back as a block. The two lines that matter most are FieldName and, for checkboxes and radios, FieldStateOption — those state options are the only values that will actually toggle the field:
---
FieldType: Text
FieldName: applicant_full_name
FieldNameAlt: Applicant Full Name
---
FieldType: Button
FieldName: consent_marketing
FieldStateOption: Off
FieldStateOption: Yes
---
FieldType: Choice
FieldName: account_type
FieldStateOption: Savings
FieldStateOption: CurrentNote that consent_marketing expects the literal string 'Yes', not true and not '1'. Send anything else and the checkbox silently stays off. That single gotcha is behind most of the 'my fill worked but the boxes are blank' bug reports I have had to chase down.
How do I fill and flatten the form with pdftk?
pdftk fills from an FDF (Forms Data Format) file. You generate the FDF, then call fill_form with the flatten keyword so the values are burned into the page and the form fields are removed. Without flatten, the recipient can open the PDF and edit your values, which is unacceptable for a signed loan document or a tax filing.
<?php
namespace App\Services;
class PdfFormFiller
{
public function fill(string $template, array $data, string $output): void
{
$fdf = $this->buildFdf($data);
$fdfPath = tempnam(sys_get_temp_dir(), 'fdf');
file_put_contents($fdfPath, $fdf);
$cmd = sprintf(
'pdftk %s fill_form %s output %s flatten',
escapeshellarg($template),
escapeshellarg($fdfPath),
escapeshellarg($output)
);
exec($cmd . ' 2>&1', $out, $code);
unlink($fdfPath);
if ($code !== 0) {
throw new \RuntimeException('pdftk failed: ' . implode("\n", $out));
}
}
private function buildFdf(array $data): string
{
$fields = '';
foreach ($data as $name => $value) {
// Escape the FDF metacharacters: backslash first, then ( and )
$value = str_replace(['\\', '(', ')'], ['\\\\', '\\(', '\\)'], (string) $value);
$fields .= "<< /T ({$name}) /V ({$value}) >>\n";
}
return "%FDF-1.2\n1 0 obj\n<< /FDF << /Fields [\n{$fields}] >> >>\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF";
}
}Call it with your field map. The keys must match dump_data_fields exactly, and the checkbox value must be the FieldStateOption string:
$filler = new App\Services\PdfFormFiller();
$filler->fill(
template: storage_path('forms/loan-application.pdf'),
data: [
'applicant_full_name' => 'Md Raihan Hasan',
'account_type' => 'Savings',
'consent_marketing' => 'Yes', // the export value, not true
],
output: storage_path('app/filled/loan-12891.pdf')
);The PDF already knows where everything goes — your only job is to hand it the right field names and the exact export values, then flatten so nobody can change them.
What if I cannot install pdftk on the server?
Shared hosting and locked-down containers often will not let you shell out to a binary. In that case, fill in pure PHP with FPDM, a thin layer over FPDI that imports the template and writes form values directly. It is Composer-installable and needs no external process. FPDM ships as a classmap, not a PSR-4 namespace, so the class lives in the global namespace — reference it as \FPDM, with the leading backslash, from inside any namespaced Laravel code:
<?php
require 'vendor/autoload.php';
// FPDM registers in the GLOBAL namespace via Composer classmap.
// From namespaced code, reference it as \FPDM (leading backslash).
$pdf = new \FPDM('forms/loan-application.pdf');
$pdf->Load([
'applicant_full_name' => 'Md Raihan Hasan',
'account_type' => 'Savings',
'consent_marketing' => 'Yes',
], false); // 2nd arg: false = ISO-8859-1 source data, true = UTF-8
$pdf->Merge();
$pdf->Output('F', 'filled/loan-12891.pdf'); // FPDF signature: Output(dest, name); 'F' writes to diskFPDM is free but has limits: it does not flatten, and it struggles with newer PDF versions and compressed object streams (you may need to run the template through pdftk or qpdf once to decompress it first). For high-volume production work — real flattening, appearance streams, and full Unicode — the commercial SetaPDF-FormFiller is the tool I reach for. It costs money, but it handles the edge cases that otherwise eat days. This kind of batch document generation belongs in a queued job; I cover that pattern in Laravel queue workers with Supervisor.
Which gotchas actually bite in production?
- Not flattening. An unflattened fill is still an editable form. Always pass flatten with pdftk, or generate appearance streams with a tool that supports it. Recipients editing a 'final' document is a real incident, not a hypothetical.
- Wrong checkbox/radio value. Send the FieldStateOption string verbatim. 'Yes', '1', and 'On' are all common and form-specific — there is no universal default.
- Non-Latin text. Bengali, Arabic, and CJK characters need the field's font to embed those glyphs, and the data passed as UTF-8 (FPDM's second Load argument, or a SetaPDF font subset). If the AcroForm field uses Helvetica, non-Latin characters simply will not render.
- Missing appearance streams. Some viewers (notably mobile and in-browser PDF readers) only render filled values if NeedAppearances is set or appearance streams are generated. pdftk's flatten generates them; setting raw values alone may show blanks in Chrome's viewer.
- Compressed or XFA forms. If dump_data_fields returns nothing, the form may be XFA (LiveCycle), not AcroForm — those need a different toolchain entirely.
Once you accept that the template owns the layout and you only own the data, fillable pdf forms in php stop being scary. Read the field names with dump_data_fields, build a clean field-to-value map, fill, then flatten. Wrap that in a service class, run it through a queue for batch jobs, and check the first ten records by hand — every blank field you find traces straight back to a name typo or a wrong export value, and you will fix it in seconds because you have the dump sitting in front of you.

