#Signing On-Demand (DomPDF / Generated PDFs)
Use this when your model has no stored PDF and you render one on the fly, like a DomPDF preview from a Blade view.
The package only signs files on disk, because a re-render never has exactly the same bytes. So you render the PDF once on first sign, save it, and the package takes it from there.
Fixed-layout document? A PDF template with the
HasPdfTemplatetrait does all of this for you.
#How it works
- The user clicks Sign Document.
- The package calls
$record->getSignablePdfPath(). Your model renders and saves the PDF if it isn't on disk yet. - The PDF driver (
signature.pdf_driver,fpdiortcpdf) stamps the signature, embeds a PKCS#7 signature and saves the signed copy. - Your "View PDF" action serves the signed copy once it exists.
The saved PDF is the frozen version of the document. Editing the record afterwards doesn't change what was signed.
#Step 1 — Return raw PDF bytes
Your PDF service needs a public method that returns the PDF as a string. The preview route and signing can then share it.
// app/Services/RequisitionIssueSlipPdfService.php
public function renderBinary(RequisitionIssueSlip $slip): string
{
// ... existing logic ...
return $pdf->output();
}#Step 2 — Implement Signable and save the PDF lazily
// app/Models/RequisitionIssueSlip.php
use App\Services\RequisitionIssueSlipPdfService;
use Illuminate\Support\Facades\Storage;
use Kukux\DigitalSignature\Contracts\Signable;
use Kukux\DigitalSignature\Traits\HasSignatures;
class RequisitionIssueSlip extends Model implements Signable
{
use HasSignatures;
public function getSignableTitle(): string
{
return 'RIS-'.($this->ris_number ?: $this->getKey());
}
public function getSignableId(): int|string
{
return $this->getKey();
}
public function getSignablePdfPath(): string
{
$disk = Storage::disk(config('signature.storage_disk'));
$path = "ris/{$this->getKey()}.pdf";
if (! $disk->exists($path)) {
$disk->put($path, app(RequisitionIssueSlipPdfService::class)->renderBinary($this));
}
return $path;
}
}Path rules:
- Return a file on
signature.storage_disk(envSIGNATURE_DISK, defaultlocal): disk-relative (documents/12.pdf), or absolute inside that disk's root (Storage::disk(...)->path('documents/12.pdf'), which is whatHasPdfTemplatereturns). Both are normalised before hashing and signing. A path outside the disk (storage_path('tmp/…')on another disk) is not: the drivers read throughStorage::disk(). - Use a stable name, like the primary key. No timestamps or slugs.
- Keep the disk private (
local,private,s3) so unsigned PDFs aren't public. - Throw on failure. The return type is
string; returningnullgives aReturn value must be of type string, null returnedTypeError. Exceptions show up as a "Signing failed" notification.
#Clearing the cached PDF
Never re-render after signing. The new file won't match the signature's document_hash, so verification fails.
If the record can change before signing, delete the cached file on update:
protected static function booted(): void
{
static::updated(function (self $slip) {
if ($slip->isSigned()) {
return; // never invalidate after signing
}
Storage::disk(config('signature.storage_disk'))->delete("ris/{$slip->getKey()}.pdf");
});
}isSigned() comes from the HasSignatures trait and is true once any signature on the record has status signed.
#Step 3 — Show the signed copy in "View PDF"
The signed PDF lands in signature.signed_docs_path (default signed-docs) as <name>_signed_<timestamp>.pdf. Its path is saved on the Signature row as signed_document_path.
// app/Filament/Resources/.../Pages/ViewRequisitionIssueSlip.php
use Filament\Actions\Action;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\Facades\URL;
use Kukux\DigitalSignature\Filament\Actions\SignDocumentAction; // resolves to the right Filament version
protected function getHeaderActions(): array
{
return [
SignDocumentAction::make()
->stampAt(page: 1, x: 100, y: 650, w: 200, h: 80),
Action::make('generate_pdf')
->label('View PDF')
->icon(Heroicon::OutlinedDocumentArrowDown)
->url(function (): string {
$record = $this->getRecord();
$signedPath = $record->signatures()
->whereNotNull('signed_document_path')
->latest('signed_at')
->value('signed_document_path');
return $signedPath
? URL::temporarySignedRoute('requisition-issue-slips.pdf.signed', now()->addMinutes(30), ['path' => $signedPath])
: route('requisition-issue-slips.pdf.preview', ['requisitionIssueSlip' => $record]);
}, shouldOpenInNewTab: true),
];
}stampAt() uses PDF points, with y measured from the bottom of the page. Leave it off to let the signer place the signature.
Then add a route and controller that stream the file:
// routes/web.php
Route::get('/ris/signed/{path}', [SignedPdfController::class, 'show'])
->where('path', '.*')
->name('requisition-issue-slips.pdf.signed')
->middleware(['auth', 'signed']);
// app/Http/Controllers/SignedPdfController.php
public function show(string $path)
{
$disk = Storage::disk(config('signature.storage_disk'));
abort_unless($disk->exists($path), 404);
// authorize: look up the Signature by signed_document_path and check your policy
return $disk->response($path, headers: ['Content-Type' => 'application/pdf']);
}The signed middleware returns 403 for plain route() URLs. Use a signed URL as above, or drop signed and rely on your own authorization.
#Gotchas
- One signer per PDF.
SignDocumentActionalways signs the original source PDF, so two signers give you two separate signed copies. For several signatures on one PDF, use Signatory Routing. - Don't just draw the image in your Blade view. That's a visual stamp: no "Signed by" in PDF readers and no tamper detection. If that's all you need, a
signed_atcolumn is enough.
#Related
- Model Setup —
Signablecontract andHasSignaturestrait reference - Signing Workflow — full lifecycle, what
SignatureManagerdoes - PDF Templates — fixed-layout documents with
HasPdfTemplate - Security — PKCS#7, DocMDP, document integrity hashing