#PDF Templates
A PDF template declares to the plugin that the host app produces a particular kind of PDF (DTR, payslip, contract, …) and which named regions on that PDF accept a signature. The plugin uses this declaration to:
- list registered templates in the admin UI ("apply this signature to a DTR"),
- persist per-slot placement coordinates per template (so admins place signature zones once, not per record),
- render a sample preview for the placement designer.
Templates are additive. The existing Signable flow (Model Setup, Ad-hoc Signing, On-Demand PDF Signing) continues to work unchanged — templates simply give you a way to register, enumerate, and configure those signables centrally.
Status. The contract, registry and persistence, the placement designer, and the end-user signer page with real finalize wiring are all landed. Slot coordinates are now resolved from the database automatically for session-based signing — see Signatory Routing, which builds on everything here to route each slot to a specific person and carry a document through several signatories.
#When to use a template
| Scenario | Use Signable only | Add a PdfTemplate |
|---|---|---|
| One-off, ad-hoc document signing where the signer drags the signature into place each time | ✅ | – |
| Same PDF layout reused across many records (DTR, payslip, certificate) where the signature always goes in the same place | – | ✅ |
| You want named "drop zones" (e.g. Employee + In Charge) the designer can highlight as snap targets | – | ✅ |
| You want a central "PDF Templates" admin page listing what can be signed | – | ✅ |
A PdfTemplate and the underlying Signable model are not mutually exclusive — the template's renderFor($record) returns a PDF for a record that already implements Signable. Think of PdfTemplate as the category and Signable as the instance.
#The pieces
| File | What it is |
|---|---|
Contracts/PdfTemplate | Interface the host app implements per kind of PDF |
Pdf/SlotDefinition | Readonly value object describing one named signature zone |
Models/PdfTemplateSlot | Eloquent model — persisted (template, slot) coordinates |
digital_pdf_template_slots table | Stores the saved x/y/width/height per slot per template |
Services/PdfTemplateRegistry | Container singleton that holds all registered templates |
#Registering a template (plug-and-play)
The fastest path: drop an array into config/signature.php pointing at a Blade view you already have. No PHP class required.
// config/signature.php
'templates' => [
'dtr' => [
'label' => 'Daily Time Record',
'view' => 'pdf.dtr', // your existing Blade
'sample_data' => ['user' => ['name' => 'Sample User']],
'data_resolver' => fn ($record) => ['record' => $record],
'slots' => ['employee', 'in_charge'],
],
],That's it. The array gets read on boot, instantiated into a BladePdfTemplate, and registered automatically. The plugin renders the Blade via barryvdh/laravel-dompdf (auto-detected); install it once if you don't have it:
composer require barryvdh/laravel-dompdf
#What the keys mean
| Key | Required | Type | What it does | |
|---|---|---|---|---|
label | no | string | Human-readable name shown in the designer / signer UI. Defaults to titlecased key. | |
view | yes | string | Blade view name (e.g. pdf.dtr for resources/views/pdf/dtr.blade.php). | |
sample_data | no | array \ | callable | Data passed to the Blade when rendering the sample preview. Use a callable for expensive seed data. |
data_resolver | no | fn($record) => array | Maps a record to the data the Blade needs at sign time. Defaults to ['record' => $record]. | |
slots | no | array | Slot definitions — see the three accepted shapes below. | |
renderer | no | class-string | Custom PdfRenderer class if you don't want DomPDF. |
#Slot shapes
Three accepted shapes, mix freely:
// 1) Bare keys — label auto-derived (Title Case of the key)
'slots' => ['employee', 'in_charge'],
// 2) Keyed associative entries — full control over each slot
'slots' => [
'employee' => ['label' => 'Employee', 'required' => true],
'in_charge' => ['label' => 'In Charge', 'required' => true],
],
// 3) Numbered list of associative entries — useful when you want a stable order
'slots' => [
['key' => 'employee', 'label' => 'Employee', 'required' => true],
['key' => 'in_charge', 'label' => 'In Charge', 'required' => true],
],A slot may also declare signatory, role and order to bind it to a specific person on the record — see Signatory Routing.
Each slot may also carry an initial placement (page, x, y, width, height in PDF points) — they seed the placement designer the first time a slot is opened. Once an admin saves coordinates via the designer, the persisted row wins.
#Verifying it worked
After editing config and running php artisan config:clear:
app(\Kukux\DigitalSignature\Services\PdfTemplateRegistry::class)->all();
// → ['dtr' => Kukux\DigitalSignature\Pdf\BladePdfTemplate { … }]Then visit a signature's view page — the registered templates now appear as cards.
#Using a different PDF renderer
DomPDF is the default. To use Browsershot, Snappy, or anything else, implement PdfRenderer once and reference it in the template config:
// app/Pdf/BrowsershotRenderer.php
use Kukux\DigitalSignature\Pdf\Renderers\PdfRenderer;
class BrowsershotRenderer implements PdfRenderer
{
public function render(string $view, array $data, string $destinationPath): string
{
\Spatie\LaravelPdf\Facades\Pdf::view($view, $data)->save($destinationPath);
return $destinationPath;
}
public static function isAvailable(): bool
{
return class_exists(\Spatie\LaravelPdf\Facades\Pdf::class);
}
}
// config/signature.php
'templates' => [
'dtr' => [
'view' => 'pdf.dtr',
'renderer' => \App\Pdf\BrowsershotRenderer::class,
// …
],
],#Implementing a template (full class)
When you need more than the config form gives you — conditional slots, complex data resolution, multi-source sample data — implement the contract directly. Example for a DTR (Daily Time Record):
namespace App\Pdf;
use App\Models\Dtr;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use Kukux\DigitalSignature\Contracts\PdfTemplate;
use Kukux\DigitalSignature\Pdf\SlotDefinition;
class DtrTemplate implements PdfTemplate
{
public function key(): string
{
return 'dtr';
}
public function label(): string
{
return 'Daily Time Record';
}
/**
* Named regions on the DTR PDF that can accept a signature.
* Defaults are starting suggestions only — admins override via
* the placement designer (or by writing to digital_pdf_template_slots).
*/
public function slots(): array
{
return [
new SlotDefinition(
key: 'employee',
label: 'Employee',
defaultPage: 1,
defaultX: 135,
defaultY: 90,
defaultWidth: 200,
defaultHeight: 40,
required: true,
),
new SlotDefinition(
key: 'in_charge',
label: 'In Charge',
defaultPage: 1,
defaultX: 520,
defaultY: 90,
defaultWidth: 200,
defaultHeight: 40,
required: true,
),
];
}
/**
* Sample PDF used to preview the layout in the designer.
* Should be deterministic so the rasterized preview can be cached.
*/
public function renderSample(): string
{
$disk = Storage::disk(config('signature.storage_disk'));
$rel = 'samples/dtr.pdf';
if (! $disk->exists($rel)) {
$disk->put($rel, app(DtrPdfRenderer::class)->renderBinarySample());
}
return $disk->path($rel);
}
/**
* Render the production PDF for a specific record at sign time.
* Same contract as Signable::getSignablePdfPath() — return an
* absolute filesystem path the signer driver can read.
*/
public function renderFor(Model $record): string
{
assert($record instanceof Dtr);
$disk = Storage::disk(config('signature.storage_disk'));
$rel = "generated/dtr/{$record->getKey()}.pdf";
if (! $disk->exists($rel)) {
$disk->put($rel, app(DtrPdfRenderer::class)->renderBinary($record));
}
return $disk->path($rel);
}
}#Slot coordinates
- Units are PDF points (1 pt = 1/72 inch). US Letter landscape is 792 × 612 pt; A4 landscape is 842 × 595 pt.
yis measured from the bottom of the page, not the top. This is PDF-native and the same convention the existingsignature_positionstable uses. A signature line near the bottom of the page typically sits aroundy = 60–120.- Defaults are seeds, not law. They're only used when no row exists in
digital_pdf_template_slotsfor that (template, slot) yet. Once an admin saves coordinates via the designer (step 4), the saved values win.
#Registration paths
Three places to register, all merge into the same registry. Each accepts the plug-and-play array form, class strings, or PdfTemplate instances — mix as you like.
#1. Eager — via config
// config/signature.php
'templates' => [
// Plug-and-play (array form)
'dtr' => [
'view' => 'pdf.dtr',
'slots' => ['employee', 'in_charge'],
],
// Full class (class-string form)
\App\Pdf\PayslipTemplate::class,
],Loaded by SignatureServiceProvider::boot(). Survives across panels and CLI.
#2. Fluent — on the Filament panel
// app/Providers/Filament/AdminPanelProvider.php
->plugins([
\Kukux\DigitalSignature\SignaturePlugin::make()
->templates([
'dtr' => [
'view' => 'pdf.dtr',
'slots' => ['employee', 'in_charge'],
],
\App\Pdf\PayslipTemplate::class,
]),
])Useful when registration should differ between panels (e.g. an HR panel only sees DTRs).
#3. Runtime — from any service provider
use Kukux\DigitalSignature\Services\PdfTemplateRegistry;
public function boot(): void
{
$registry = app(PdfTemplateRegistry::class);
// Array form via dedicated helper
$registry->registerBlade('dtr', [
'view' => 'pdf.dtr',
'slots' => ['employee', 'in_charge'],
]);
// Class or instance
$registry->register(\App\Pdf\PayslipTemplate::class);
}Good for package integrations or conditional registration.
Registration is idempotent and keyed by the template key. Registering the same key twice replaces, doesn't duplicate.
#Reading the registry
use Kukux\DigitalSignature\Services\PdfTemplateRegistry;
$registry = app(PdfTemplateRegistry::class);
$registry->has('dtr'); // bool
$registry->find('dtr'); // ?PdfTemplate (null if missing)
$registry->get('dtr'); // PdfTemplate (throws if missing)
$registry->all(); // array<string, PdfTemplate>get() is the version to use when the key came from a trusted source (e.g. a template_key row from digital_pdf_template_slots). find() is for user input / nullable lookups.
#The placement designer
The plugin ships an interactive designer that renders the template's sample PDF and lets an admin drag/resize each slot onto the page. Saves are persisted to digital_pdf_template_slots, so once you've placed a slot it sticks for every future record of that template.
#Opening the designer
The Filament page is registered automatically by SignaturePlugin when the plugin is added to a panel. The slug pattern is:
/<panel-path>/signature-templates/{templateKey}/designFor a panel mounted at /admin and a template with key() === 'dtr', that's:
/admin/signature-templates/dtr/designThe page is not added to the sidebar — link to it from your own UI. Typical entry points:
- A "Design layout" header action on the host app's
DtrResource - A link inside the signature library card that opens this designer for the related template
use Filament\Actions\Action;
Action::make('design_layout')
->label('Design layout')
->icon('heroicon-o-rectangle-group')
->url(fn () => route('filament.admin.pages.signature-templates.{template-key}.design', [
'templateKey' => 'dtr',
]))
->openUrlInNewTab(false);#How it works under the hood
Browser Backend
┌────────────────────────────────────┐ ┌──────────────────────────┐
│ PdfDesignerIsland (React) │ GET /meta │ DesignerController::meta │
│ ├─ fetches meta + saved slots ────┼────────────────────►│ - reads PdfTemplate │
│ ├─ renders <img src=page/1> │ GET /pages/1 │ - reads PdfTemplateSlot │
│ ├─ overlays SlotBox per slot │◄────────────────────┤ - returns PDF point │
│ ├─ user drags / resizes / nudges │ PNG │ dimensions per page │
│ └─ Save → POST /slots/{slot} ─────┼────────────────────►│ DesignerController::page │
└────────────────────────────────────┘ │ - rasterizes via Imagick│
│ (or RendersSample…) │
├──────────────────────────┤
│ DesignerController::save │
│ - upsert by │
│ (template_key, slot) │
└──────────────────────────┘The React island handles the CSS-pixel ↔ PDF-point math (including the y-axis flip) so your slot definitions and saved rows are always in PDF coordinates ready for the signer driver.
#Imagick prerequisite (or an alternative)
By default the designer rasterizes the sample PDF via Imagick + Ghostscript. If your host doesn't have those, implement RendersSamplePageImage on your template:
use Kukux\DigitalSignature\Contracts\PdfTemplate;
use Kukux\DigitalSignature\Contracts\RendersSamplePageImage;
class DtrTemplate implements PdfTemplate, RendersSamplePageImage
{
// ... existing PdfTemplate methods ...
public function renderSampleAsImage(int $page): string
{
// Return an absolute path to a PNG/JPEG of $page.
// For DomPDF / Spatie Browsershot / Snappy users: render directly to PNG.
return app(DtrPdfRenderer::class)->renderPageAsPng(page: $page);
}
public function sampleImagePageCount(): int
{
return 1;
}
}When a template implements RendersSamplePageImage, the controller skips the rasterizer and uses your image directly. Note: in that mode the plugin assumes image pixel dimensions equal PDF point dimensions, so render at 72 DPI for accurate placement (or stick with the Imagick path).
#Designer DPI
The Imagick render DPI is configurable:
SIGNATURE_DESIGNER_DPI=144 # default; raise to 200+ for crisp big monitors
// config/signature.php
'designer' => [
'dpi' => env('SIGNATURE_DESIGNER_DPI', 144),
],Higher DPI = sharper preview but slower first render and larger cache files. The cache is keyed by (pdf path, mtime, dpi) so changing DPI invalidates cleanly.
#Seeding slots without the designer
You can still write coordinates directly from a seeder or Tinker:
use Kukux\DigitalSignature\Models\PdfTemplateSlot;
PdfTemplateSlot::updateOrCreate(
['template_key' => 'dtr', 'slot_key' => 'employee'],
['page' => 1, 'x' => 135, 'y' => 90, 'width' => 200, 'height' => 40],
);The SlotDefinition::$defaultX/Y/... fields still serve as the initial position the designer seeds when no saved row exists yet.
#The signer page
In addition to the admin designer, the plugin ships an end-user signer page — the SignFlow-style view where a user drops their signature onto a target PDF and clicks "Finish & Save".
#URL pattern
/<panel-path>/signature-templates/{templateKey}/sign/{signatureUuid}The route is built for you from the Signature view page's card grid (view-signature-with-templates.blade.php). Each card's body click navigates to the signer with that signature's UUID pre-bound to the URL.
#What the page does
PdfSigningIsland (React)
├─ Top bar: Template label [Cancel] [Finish & Save]
├─ Canvas: Rasterized PDF page with SlotBox overlays
│ ├─ Active signature image is rendered inside each placed slot
│ └─ Page selector when the template has > 1 page
├─ Slot picker (chips below the canvas):
│ ├─ "+ Employee", "+ In Charge", … one chip per declared slot
│ └─ Click adds the slot at the canvas center; click again removes
└─ Bottom strip: Your stored signatures
├─ Active signature highlighted with primary ring
└─ Up to 12 other primary signatures (chip = click to swap)The frontend talks to two endpoints, both prefixed by signer/:
| Method | Route name | Purpose |
|---|---|---|
| GET | signature.pdf-templates.signer.meta | Bootstrap: template + page dims + saved slots + user's signature library |
| POST | signature.pdf-templates.signer.finalize | Submit chosen placements + finalize signing |
#Finalize: two modes
The finalize endpoint has two response paths depending on whether the URL carries a ?signable=ID query param.
Acknowledgement mode (no ?signable). Validates the placements and echoes them back. Useful when you want to exercise the UI without producing a real PDF — convenient while calibrating slot positions or testing the rendering pipeline.
Production mode (with ?signable=ID). Runs the real signing pipeline:
- Resolves the host model via the template's
signableconfig:($template->getSignableClass())::find($id). - Asserts the model implements
Signable. Use theHasPdfTemplatetrait to satisfy that in two lines. - Renders the production PDF via
$template->renderFor($record). - Calls
SignatureManager::storeForDocument()— creates thedigital_signatureschild row +signature_positionsrow from the placement. - Calls
SignatureManager::embedAndFinalize()— stamps the signature image, embeds PKCS#7 + DocMDP, writes the signed PDF, returns the disk-relative path. - Returns
{ status: 'signed', signature_uuid, signed_document_path, signed_at }so the UI can offer a download.
The certificate password is read from the source signature row (stored encrypted at registration time), so the signer doesn't have to enter it again at sign time.
#Making your model signable in 2 lines
use Illuminate\Database\Eloquent\Model;
use Kukux\DigitalSignature\Concerns\HasPdfTemplate;
use Kukux\DigitalSignature\Contracts\Signable;
class Dtr extends Model implements Signable
{
use HasPdfTemplate;
protected string $signaturePdfTemplate = 'dtr'; // matches the config key
}That's all that's needed. The trait implements getSignableTitle(), getSignablePdfPath(), and getSignableId() for you — getSignablePdfPath() calls back into the registered template's renderFor($this). Override any of the three methods if you need different behavior.
#Linking to the signer from your own resource
The signer page slug is signature-templates/{templateKey}/sign/{signatureUuid}. The target record is passed via ?signable=ID.
A typical Filament resource header action looks like this:
use Filament\Actions\Action;
use Kukux\DigitalSignature\Filament\Pages\PdfTemplateSigner;
use Kukux\DigitalSignature\Models\Signature;
Action::make('sign_with_my_signature')
->label('Sign')
->icon('heroicon-o-pencil-square')
->url(function ($record) {
$signature = Signature::query()
->where('user_id', auth()->id())
->whereNull('signable_id')
->where('status', 'active')
->latest('id')
->firstOrFail();
return PdfTemplateSigner::getUrl([
'templateKey' => 'dtr',
'signatureUuid' => $signature->uuid,
]).'?signable='.$record->getKey();
});The user lands on the signer page with their signature pre-selected, the DTR record loaded, and a "Finish & Save" button that produces a real signed PDF.
#Multi-slot signing
A single finalize call still applies one placement: the free-form signer
path signs the record's own PDF, so calling it N times would produce N separate
signed copies rather than one document with N stamps.
Documents that genuinely need several signatures go through a signing
session instead, which freezes the PDF once and chains each signature onto
the previous one's output. The signer page participates in that flow by passing
?request=<id> (the inbox links it for you); finalize then routes through
SigningSessionManager, which enforces the slot ownership and sequencing rules.
See Signatory Routing for the full picture, including
what progressive and incremental signing modes each guarantee.
#What's still ahead
- Multi-stamp in a single cryptographic pass — true PAdES incremental signing, so one PDF carries N independently verifiable signer certificates. The session machinery is in place; it needs a signer driver implementing
SupportsIncrementalSigning, which neither bundled driver can (FPDI rewrites the document). See Signatory Routing → Signing modes.
You can use the full system today to:
- declare templates and slot definitions (array form or full class),
- open the designer for any registered template and place slots visually,
- open the signer page for any registered template + owned signature,
- sign a real document end-to-end via
?signable=IDandHasPdfTemplate, - route each slot to a person and take a document through several signatories (Signatory Routing),
- read coordinates from your own code to drive
SignatureManager::store(...)directly.
#Related
- Model Setup —
Signablecontract andHasSignaturestrait - On-Demand PDF Signing — when the PDF is generated, not stored
- Signing Workflow —
SignatureManagerAPI, events, statuses - Filament Components —
SignaturePlugin, fluent registration API - Signatory Routing — binding slots to people, signing sessions, consent models