kukux / pdf-template-builder
A drag-and-drop PDF template builder plugin for Filament Admin. Upload a PDF background, bind Eloquent model fields, and place them visually on the canvas.
Requires
- php: ^8.2
- filament/filament: ^3.0 || ^4.0 || ^5.0
- illuminate/contracts: ^11.0 || ^12.0
- illuminate/database: ^11.0 || ^12.0
- illuminate/http: ^11.0 || ^12.0
- illuminate/routing: ^11.0 || ^12.0
- illuminate/support: ^11.0 || ^12.0
- spatie/laravel-package-tools: ^1.15
Requires (Dev)
- orchestra/testbench: ^9.0
- pestphp/pest: ^2.0
- pestphp/pest-plugin-laravel: ^2.0
Suggests
- dompdf/dompdf: Install to enable PDF output via the bundled DompdfEngine. Without it, the plugin returns HTML.
- setasign/fpdi: Install with tecnickcom/tcpdf to enable FpdiEngine — true PDF overlay that preserves the original uploaded PDF.
- tecnickcom/tcpdf: Required by FpdiEngine for unicode-aware PDF generation when stamping fields onto the original PDF.
README
Installation
1. Require the package
composer require kukux/pdf-template-builder
2. Publish and run the migration
php artisan vendor:publish --tag=pdf-template-builder-migrations php artisan migrate
3. Publish assets
The builder UI is a React SPA bundled with Vite. The package ships pre-built JS in resources/dist/. Publish to public/:
php artisan vendor:publish --tag=pdf-template-builder-assets
Re-run this command after each package update to get the latest bundle.
For maintainers / contributors — to rebuild the bundle from JSX source:
npm install npm run build # one-shot build → resources/dist/pdf-builder.js npm run dev # rebuild on save
ⓘ Filament v3 / v4 / v5 auto-detection
The plugin ships two parallel Resource implementations and aliases the canonical class name to the right one based on the Filament major version installed in your project.
| Detected | Class actually loaded |
|---|---|
Filament v3 (no Filament\Schemas\Schema class) |
Kukux\PdfTemplateBuilder\Filament\Resources\V3\PdfTemplateResource |
Filament v4 / v5 (Filament\Schemas\Schema exists) |
Kukux\PdfTemplateBuilder\Filament\Resources\V4\PdfTemplateResource |
Detection happens in PdfTemplateBuilderServiceProvider::register() via class_alias, before Filament's panel resolves any resource. You always reference the canonical Kukux\PdfTemplateBuilder\Filament\Resources\PdfTemplateResource::class — never the V3/V4 variants directly.
If you ever see Class "Kukux\…\PdfTemplateResource" not found after upgrading, run composer dump-autoload. PSR-4 lookup needs to miss so the alias can be consulted, and a stale optimized classmap can short-circuit that.
4. Register the plugin in your Filament panel
In your PanelProvider (e.g. app/Providers/Filament/AdminPanelProvider.php):
use Kukux\PdfTemplateBuilder\PdfTemplateBuilderPlugin; public function panel(Panel $panel): Panel { return $panel // ...your existing config... ->plugin( PdfTemplateBuilderPlugin::make() ); }
5. Register your models (optional but recommended)
Configure the Eloquent models whose fields will appear in the builder sidebar:
->plugin( PdfTemplateBuilderPlugin::make() ->models([ 'invoice' => [ 'label' => 'Invoice', 'icon' => 'receipt', 'class' => App\Models\Invoice::class, 'fields' => [ ['key' => 'invoice.number', 'label' => 'Number', 'type' => 'text', 'sample' => 'INV-0001'], ['key' => 'invoice.issued_at', 'label' => 'Issued at', 'type' => 'date', 'sample' => 'Jan 1, 2026'], ['key' => 'invoice.due_at', 'label' => 'Due at', 'type' => 'date', 'sample' => 'Feb 1, 2026'], ['key' => 'invoice.subtotal', 'label' => 'Subtotal', 'type' => 'currency', 'sample' => '$1,000.00'], ['key' => 'invoice.tax', 'label' => 'Tax', 'type' => 'currency', 'sample' => '$100.00'], ['key' => 'invoice.total', 'label' => 'Total', 'type' => 'currency', 'sample' => '$1,100.00'], ['key' => 'invoice.notes', 'label' => 'Notes', 'type' => 'longtext', 'sample' => 'Thank you.'], ['key' => 'invoice.line_items','label' => 'Line items','type' => 'table', 'sample' => '[table]'], ], 'relations' => [ 'customer' => [ 'label' => 'Customer', 'fields' => [ ['key' => 'customer.name', 'label' => 'Name', 'type' => 'text', 'sample' => 'Acme Inc.'], ['key' => 'customer.email', 'label' => 'Email', 'type' => 'text', 'sample' => 'hi@acme.co'], ['key' => 'customer.address', 'label' => 'Address', 'type' => 'longtext', 'sample' => '221B Baker St'], ], ], ], ], ]) )
Alternatively, configure them in config/pdf-template-builder.php after publishing the config:
php artisan vendor:publish --tag=pdf-template-builder-config
6. (Optional) Configure storage
By default PDF backgrounds are stored on the public disk under pdf-templates/backgrounds/. Override via the plugin or .env:
PDF_TEMPLATE_DISK=s3
Or via the plugin:
PdfTemplateBuilderPlugin::make()->disk('s3')->uploadPath('my-path/pdfs')
What gets added to your app
- Navigation: A "PDF Templates" item in your Filament sidebar.
- Template list: Browse, search, and filter saved templates.
- Create form: Upload a background PDF, pick a model, set page size.
- Visual builder: Drag fields from the sidebar onto the PDF canvas, resize them, style their typography, and save the layout.
- API routes at
/filament-pdf-builder/api/*(web + auth middleware). pdf_templatestable in your database.
Generating PDFs
1. Install a PDF engine (optional but recommended)
The plugin auto-detects dompdf/dompdf and uses it for output. Without it, render() falls back to HTML (browser print → save as PDF still works).
composer require dompdf/dompdf
2. Drop a "Generate PDF" button into any Filament page
On a ViewRecord / EditRecord page (header action):
use Kukux\PdfTemplateBuilder\Filament\Actions\GeneratePdfAction; protected function getHeaderActions(): array { return [ GeneratePdfAction::make() ->template('invoice-default'), // by template name ]; }
As a row action on a Resource table:
use Kukux\PdfTemplateBuilder\Filament\Actions\GeneratePdfTableAction; ->actions([ GeneratePdfTableAction::make() ->template(1), // by template id ])
Dynamic template selection:
GeneratePdfAction::make() ->templateUsing(fn ($record) => $record->is_quote ? 'quote' : 'invoice') ->withContexts(fn ($record) => ['org' => $record->organization]);
3. Programmatic rendering
use Kukux\PdfTemplateBuilder\Models\PdfTemplate; $template = PdfTemplate::where('name', 'invoice-default')->firstOrFail(); $invoice = Invoice::find(42); // Get the response (PDF if dompdf is installed, else HTML) return $template->stream($invoice); // Or just the rendered HTML: $html = $template->render($invoice);
4. Field token resolution
Fields placed in the builder have a key like invoice.number or customer.email. At render time:
- The leading segment matching the template's
model_keyis stripped, then the remainder is resolved against the record viadata_get(). - Otherwise the full key is resolved against the record (so relations work:
customer.name→$invoice->customer->name). - Pass extra named contexts via
->withContexts(['org' => $org])and reference them withorg.namein field tokens.
5. Choosing a PDF engine
The plugin ships three engines. Pick based on your needs:
| Engine | When to use | Requires |
|---|---|---|
HtmlEngine (default fallback) |
Quick preview; user prints to PDF from browser. | — |
DompdfEngine (auto-detected) |
Generic HTML→PDF. Good for templates with no background or simple backgrounds. | composer require dompdf/dompdf |
FpdiEngine |
You uploaded a designed PDF as the background and want the original PDF preserved exactly. Stamps fields directly onto the original page. | composer require setasign/fpdi tecnickcom/tcpdf |
Wire your choice on the plugin:
use Kukux\PdfTemplateBuilder\Rendering\Engines\FpdiEngine; PdfTemplateBuilderPlugin::make()->engine(FpdiEngine::class)
You can also implement Kukux\PdfTemplateBuilder\Rendering\Contracts\PdfEngine (or TemplateAwarePdfEngine for non-HTML pipelines like Browsershot) and register your own.
6. Running the test suite
composer install vendor/bin/pest
The package ships with Pest tests covering routes, the field resolver, the HTML renderer, the filename pattern, and basic model persistence. Run them after every change.
7. Authorization
Templates use a default-permissive PdfTemplatePolicy. Override it in your AuthServiceProvider:
Gate::policy( \Kukux\PdfTemplateBuilder\Models\PdfTemplate::class, \App\Policies\PdfTemplatePolicy::class, );