nowo-tech/pdf-signable-bundle
Symfony bundle for adding digital signature support to PDF documents, enabling signing, verifying, and managing signed PDFs within your application. Simplifies PDF signing workflows and integrates with existing Symfony services.
Install and register
composer require nowo-tech/pdf-signable-bundle
Flex may register it automatically; otherwise add it to config/bundles.php. There is no documented config file contract in the package metadata, so before writing any YAML, open src/DependencyInjection/Configuration.php and the README — the config tree is the authoritative list of options, not blog posts.
Publish the assets first — this is step zero
The bundle's value is a PDF.js viewer plus drag-and-drop boxes, which means JavaScript and a worker file must be reachable from the browser. Do this before you touch a form, otherwise you will spend an hour debugging "nothing renders":
php bin/console assets:install --symlink
If you use AssetMapper or Webpack Encore, confirm how the bundle expects its entrypoint to be loaded (check src/Resources/assets for the source and src/Resources/public/build for what actually ships). Treat public/build as opaque output — never patch it.
Where to look first, in order
README.md — the only place with a working end-to-end example.src/Form/ — the form type you embed. Its configureOptions tells you what the field needs (PDF source, viewer options, whether the AcroForm editor path is enabled).src/Resources/views/ — the Twig template and the data attributes that boot the JS.src/Resources/assets/ — the TypeScript that wires PDF.js to the box overlay; this is where coordinate conversion lives.composer.json — PHP/Symfony constraints. The bundle targets PHP >= 8.2 and Symfony 7/8; do not try it on a Symfony 5.4 app.First use case to build
A back-office form: upload a PDF, render it in the viewer, drag one signature box onto page 1, submit, and persist the box as JSON on an entity. Nothing more. Do not start with multi-signer workflows or AcroForm editing — you need to prove the asset pipeline, the form round-trip, and the coordinate round-trip separately, and they fail in different ways.
Embed the field in your own form, don't build a page around it
The bundle gives you a form type, not a controller or a full UI. The idiomatic integration is to add the field to an existing Symfony form and let your own controller handle persistence:
$builder->add('signatureLayout', PdfSignableType::class, [
'pdf' => $document->getPdfPath(),
// viewer/editor options — read Configuration.php for exact keys
]);
Keeping the field embedded means you inherit your app's CSRF, validation, and security rather than fighting a bundle-provided controller.
Persist coordinates normalized, render absolute
Store each box as {page, x, y, w, h} in normalized 0–1 space relative to the page's visible box, plus the page rotation. Absolute PDF points look convenient until you render a thumbnail at a different scale or a print preview opens. Normalizing once at capture time makes every downstream consumer (thumbnail, signing library, audit view) a pure multiply.
Two-phase workflow: capture, then consume
The bundle solves coordinate capture, not cryptographic signing. The natural workflow is:
Keep phases 2 and 3 in separate services. The moment you let the coordinate capture code also stamp signatures, you lose the ability to re-open a document and adjust placement without re-signing.
Treat the AcroForm editor as opt-in per document
The AcroForm editor is described as optional and it mutates the PDF structure. Gate it behind a config flag and a per-document decision — flat/scanned PDFs with no form fields get only the drag-and-drop path. Always write AcroForm output to a new path, never over the uploaded original.
Use a data transformer for the JSON payload
The field submits a JSON string, not a typed object. A DataTransformer that maps JSON ↔ a SignatureLayout value object keeps your entity clean and lets you attach constraints (NotBlank, custom "boxes inside page bounds", "no overlapping boxes"). Without a transformer you end up with json_decode scattered across controllers.
Asset pipeline integration
Whatever frontend stack you use, integrate at the bundle's existing init point rather than writing a parallel bootstrap. If you run Turbo/htmx, re-initialize the viewer on turbo:load — a DOMContentLoaded-only init works on first paint and silently dies on every subsequent navigation, which reads as a flaky bug rather than a lifecycle bug.
Testing pattern
The high-value test is headless: submit the form with a coordinates payload, assert the entity persisted the right structure and that invalid boxes are rejected. Dragging is not worth an end-to-end browser test; the coordinate math is, so unit-test the normal/absolute conversion functions directly.
Vendor risk is the headline. Zero stars, zero dependents, a single maintainer, and a suspicious release date (2026-10-26). Pin an exact version in composer.json, read the source before depending on it in production, and be prepared to vendor the form type into your app if the project goes quiet. Budget for that fork; do not assume you'll get upstream fixes.
PDF.js worker 404 is the #1 failure. Symptoms: blank canvas, Setting up fake worker warning. Causes: assets not installed, wrong public path, or a CSP that blocks the worker. If you run a strict Content-Security-Policy, you likely need worker-src 'self' blob: and possibly blob: under script-src.
Viewer/worker version skew. If you load PDF.js from a CDN elsewhere in your app, the version must match the worker the bundle ships. Two PDF.js copies on one page produces confusing, non-obvious breakage.
Coordinate origin flip. PDF space has its origin at bottom-left; the DOM has it at top-left. Every conversion must flip y, and must account for the page's CropBox offset versus MediaBox — a cropped PDF will place every box offset by the crop margin if you assume MediaBox. Rotation (/Rotate 90/180/270) compounds this: store the rotation alongside the coordinates and apply it at render, don't bake it into stored values.
Units. PDF units are points (72/inch); the DOM is CSS pixels scaled by the viewer zoom. Never persist pixel values captured at whatever zoom the user happened to be at.
Form field not mapped. If the field holds only client-side state, mark it 'mapped' => false and read it from $form->get(...)->getData() in the controller. Otherwise Symfony throws a transformation failure on an unmapped property.
Large PDFs. The viewer lazy-loads pages but the uploaded file still crosses PHP's upload_max_filesize / post_max_size and any memory limit you set for the request. Cap page count in a validator with a clear error message rather than letting it OOM.
Extension points, in preference order: decorate services via services.yaml → override the Twig template through your app's bundle-override path → contribute an upstream patch. Do not edit files under vendor/, and do not edit src/Resources/public/build even in a fork — rebuild from src/Resources/assets.
Debugging order when nothing works: (1) browser console for PDF.js errors, (2) Network tab for 404s on the worker/viewer bundle, (3) view-source the rendered field to confirm the data attributes the JS expects are present, (4) Symfony profiler for form transformation errors. In that order — it's almost always (1) or (2).
How can I help you explore Laravel packages today?