Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Snappy Laravel Package

barryvdh/laravel-snappy

Laravel wrapper for Snappy that generates PDFs and images from HTML using wkhtmltopdf and wkhtmltoimage. Render Blade views or raw markup and configure page size, orientation, headers, footers, and custom binary options through a simple, familiar Laravel API.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit. barryvdh/laravel-snappy is a thin adapter, not an engine. It registers a service provider, exposes SnappyPdf/SnappyImage facades, and binds snappy.pdf.wrapper / snappy.image.wrapper as stateful singletons into the container. The actual rendering is delegated to the wkhtmltopdf / wkhtmltoimage binaries via knplabs/knp-snappy, which shells out through proc_open. For a TPM this reframes the decision: we are not adopting a PHP library with a bounded blast radius, we are adopting an OS-level binary dependency with a PHP convenience layer on top. The Laravel integration itself is idiomatic and low-friction; the architecture risk lives entirely one layer down.

The singleton design is the one architectural detail that changes how we write code. loadView() returns $this, and setOption() accumulates onto the shared instance, so option state leaks between renders within a request and — more dangerously — across jobs inside a queue worker. That is not a bug we can wait for; it is a code convention we must mandate (app()->forgetInstance('snappy.pdf.wrapper') before every render, or a per-render factory wrapper). Budget for that convention in the definition of done, and put it in the PR checklist.

Integration feasibility. Composer install is trivial. Binary provisioning is the project. Concretely: install wkhtmltopdf into the container image (pinned to a distro build — Debian's patched-Qt variant — because 0.12.5 and 0.12.6 produce subtly different output), verify the binary is reachable as the PHP-FPM user, not as our shell user, and confirm proc_open is not in disable_functions. Two findings gate the whole initiative and should be checked in the first 30 minutes, before any estimation:

  1. Is proc_open permitted in the target runtime?
  2. Is the target runtime serverless? If the answer is Vapor/Lambda/Cloud Run-with-arbitrary-binaries-restricted, this is a hard no and the evaluation ends.

Both are pre-commitment questions. Discovering either after sprint planning wastes a sprint.

Technical risk. Ranked, with the TPM-relevant framing:

Risk Severity Likelihood Mitigation cost
wkhtmltopdf upstream is effectively unmaintained High Certain Structural — see below
CSS fidelity: 2010-era WebKit, no reliable Flexbox/Grid, flaky page-break-inside on <tr> High High Design constraint: table layouts, inline print CSS
Stateful singleton leaking options across renders/jobs Medium High Convention + wrapper class
SSRF / local file read if user HTML or URLs reach the renderer High Medium Sanitization policy, input allowlist
Silent asset loss without enable-local-file-access on 0.12.6+ Medium High Set in option profile, not per call site
Hung binary when timeout is left false (the shipped default) Medium Medium Set numeric timeout in config and on the job
Fonts absent on render host → DejaVu fallback, silent layout shift Medium Medium Bake fonts into image
Container drift: binary removed/upgraded, works locally, fails in prod Medium Medium Non-CI smoke test asserting %PDF- prefix and size

The dominant risk is the unmaintained upstream engine. The Laravel package is actively maintained — v1.0.3 through v1.0.5 show Laravel 11, 12, and 13 compatibility landed promptly, plus a real security fix (GHSA-92rv-4j2h-8mjj in the snappy dependency) and a PHP 8.2 deprecation fix. Credit where due: the wrapper is in better health than the thing it wraps. But a maintained adapter over a decaying engine is a shrinking asset. The 48.3 composite score and 53.1 opportunity score read to me as the scoring model correctly discounting exactly this: healthy package, unhealthy foundation. I would not treat that score as a reason to reject — I would treat it as a prompt to write down the exit plan before we start.

Key questions to resolve before commitment:

  • What is the acceptable fidelity bar for these documents? A back-office invoice tolerates table-based layout and DejaVu fallback. A customer-facing branded statement or anything requiring PDF/UA accessibility does not — and wkhtmltopdf will not clear that bar.
  • Who owns the container image, and can we pin the binary version and rebuild on a schedule? If nobody owns the base image, this becomes an orphaned dependency within two quarters.
  • Is there an existing headless-Chrome capability (Browsershot/Puppeteer, Gotenberg, spatie/laravel-pdf)? Chrome is heavier at runtime but has a maintained engine, modern CSS, and no proc_open footgun. The honest comparison is not "snappy vs nothing" — it is "snappy vs a Chrome-backed renderer." Chrome is the safer default for anything new or brand-visible; Snappy wins on memory footprint, cold-start time, and simplicity for high-volume, low-fidelity internal documents.
  • What is the volume and burst profile? Per-document ~200–500ms of child-process CPU. Ten documents an hour is free; ten thousand is a capacity planning exercise.
  • Does the registry really show zero dependents? That number looks like a data-quality artifact rather than a signal — the package is widely used in the ecosystem. Do not let a scraped field substitute for judgment, but do verify the security history ourselves rather than trusting either the star count or the dependents field.

Verdict. Green-light a timeboxed two-day spike, not a commitment. The spike renders one real invoice with real content, real logo, and real item count, in the target container, under the target user, exercising the full download path. Decision gate at the end of day two: acceptable fidelity + working binary + confirmed non-serverless runtime. If any of the three fails, escalate to the Chrome-backed alternative rather than spending a sprint fighting print CSS.

Integration Approach

Stack fit. Good fit for a conventional Laravel monolith on owned VMs or containers with a mutable image and a normal PHP-FPM/queue topology. The integration surface is small and idiomatic — one config file, two facades, two container keys — so there is no meaningful coupling into our domain layer as long as we hold the boundary described below.

The boundary matters more than the library choice: no controller renders a document. Each document type becomes a small renderer class with a testable html() method and a pdf() method. This gives us three things a TPM should care about: the HTML is unit-testable without invoking a binary (milliseconds, no infrastructure), the same template can back a browser preview route during development, and the renderer becomes the single seam where we could later swap the engine without touching call sites. That seam is the real deliverable — it converts a contentious vendor decision into a reversible one.

Migration path. Where we are replacing an existing PDF path (Dompdf, TCPDF, or hand-rolled), migrate one document type at a time, never a big-bang cutover. The reason is fidelity: print CSS is empirical, and each document type surfaces its own layout failures. Sequence by value and risk:

  1. Invoice/receipt download — highest user value, bounded scope, exercises the entire path end-to-end, and gives us the fidelity verdict on real content in week one.
  2. Internal reports / month-end packs — natural queue candidate, validates the async path and timeout configuration under load.
  3. Images / OG cards / thumbnails — SnappyImage from the same Blade partials; genuinely underrated and low-risk, since social-card fidelity tolerance is high.
  4. Anything customer-facing, branded, or accessibility-required — defer, and evaluate against the Chrome renderer separately.

Keep the previous renderer behind a feature flag per document type for one release cycle. Costs almost nothing and removes the rollback drama.

Compatibility. Strong on the Laravel axis: v1.0.3 → 11.x, v1.0.4 → 12.x, v1.0.5 → 13.x, all landed promptly, and the package is release-based rather than branch-tracking. PHP 8.2 compatibility is patched. The compatibility risks sit elsewhere:

  • Binary version vs output stability. Pin the distro package version in the image and treat a binary bump as a change requiring visual regression review, not a routine image rebuild.
  • Option profiles belong in config/snappy.php, grouped per document type and versioned with the rest of the config. Scattering setOption() calls across controllers is how you get two invoices that look different for reasons nobody can reconstruct six months later.
  • Delivery contract. save() is local-filesystem only and does not create directories. For S3 or any non-local disk, take output() and Storage::put() ourselves. Write this down once; it is the kind of detail that gets rediscovered per-feature otherwise.
  • Locale is applied at render time. App::setLocale() must be set before the view renders, or a queued multilingual invoice comes out in the worker's default language while the web page is correct — a bug that reports as "the PDF is in English" and looks like a caching problem.
  • Storage and header/footer. --header-html takes a path or URL, not a string, so any real header needs a temp file with finally { unlink() }. For simple page numbering, the built-in footer-center 'Page [page] of [topage]' is far less fragile — steer the team to it first.

Sequencing. Rough shape, assuming the spike passes:

  • Week 1: Spike → gate decision. Container image with pinned binary and baked fonts. config/snappy.php with numeric timeouts and option profiles. Renderer class pattern + the forgetInstance convention written into the PR checklist.
  • Week 2–3: Invoice path in production behind a flag. Non-CI smoke test asserting real binary output. Logging of Knp\Snappy\Exception\RuntimeException verbatim — the message contains the exact command and stderr and is almost always self-explanatory.
  • Week 4: Queue path. Job-level $timeout and config-level timeout set independently — two separate kill switches for two separate hangs. Per-job wrapper reset.
  • Week 5+: Second document type; consolidate shared partials between web and PDF views so the two never drift.
  • Continuous: An ADR recording the engine risk and the named alternative, with a revisit trigger (see below).

Operational Impact

Maintenance. This is the section that should drive the decision, and the honest answer is that maintenance cost is low but not zero, and it is concentrated in infrastructure, not code. The Composer side is a non-event — pinned version, occasional bump, no transitive dependency sprawl. The recurring work is:

  • Base image ownership. Someone must own the container image, keep the binary pinned, rebuild on a base-image CVE cadence, and re-run visual regression when the binary moves. If that owner does not exist, this dependency degrades silently and we find out when logos vanish from invoices.
  • Font provisioning. Any new font is a Dockerfile change plus a rebuild. In-container font drift produces layout shifts that present as margin bugs.
  • Visual regression. No SnappyPdf::fake() exists. We get 90% of coverage from asserting the rendered HTML in fast unit tests, plus a facade swap (SnappyPdf::swap($mock)) when we need to assert response wiring. But the failure mode that mocks cannot catch is "the binary vanished from the image." That requires a separate, non-CI-or-nightly smoke test that invokes the real binary against a fixture and asserts the output starts with %PDF- and exceeds ~10KB. That single test is the highest-ROI operational investment in the whole integration; scope it in week one, not later.
  • ADR with a revisit trigger. Record the engine risk and the named fallback (headless Chrome / Gotenberg). Trigger a re-evaluation when any of: a document type requires modern CSS, an accessibility audit lands, the binary fails to build on a supported base image, or the package stops shipping Laravel-version compatibility within a release cycle.

Support. Support burden is moderate and predictable, and it clusters into a small number of recurring tickets that we should pre-empt with documentation rather than absorb one at a time:

  • "Works in my terminal, not in the app" — the FPM user's PATH or a missing binary. php artisan tinker --execute="echo shell_exec('wkhtmltopdf --version 2>&1');" is the first diagnostic step and belongs in the runbook.
  • "Logo/images missing" — almost always enable-local-file-access on 0.12.6+, or a path that isn't on the renderer's filesystem. Data URIs (base64_encode(file_get_contents(...))) sidestep the entire class and should be the default recommendation.
  • "Layout is broken" — print CSS. Flexbox/Grid unreliable, position: fixed inconsistent across pages, page-break-inside: avoid flaky on <tr>. The team should default to <table> layout and inline critical CSS in a <style> block rather than pulling a utility framework.

One support item is not routine and needs a policy decision, not a ticket: never pass user-supplied HTML or URLs to the renderer. loadHTML($request->input(...)) or loadFile($request->url) is a code-execution and SSRF vector — the binary will read file:///etc/passwd or reach cloud metadata endpoints without complaint. Restrict input to our own templates; sanitize any contributed fragment through HTMLPurifier first. Put this in the security review checklist, because it is the kind of thing a well-meaning feature developer adds in month three.

Scaling. Scaling characteristics are favorable for our likely volume and unfavorable only under load we probably don't have. Each render spawns a child process consuming ~200–500ms of CPU; process spawn overhead dominates. That means:

  • Horizontally trivial. No shared state, no workers to coordinate. Scale the queue worker pool; that's the whole story.
  • Queue everything that isn't a single user-initiated click. Month-end runs, bulk mail merges, and report packs belong in jobs, with $timeout on the job and timeout in config set independently. Pass scalars/IDs (or SerializesModels + reload), and reset the wrapper instance at the top of handle() — queue workers reuse the container across jobs, so leaked options from job N silently affect job N+1.
  • Memory is modest relative to a Chrome-backed renderer, which is the genuine reason to prefer Snappy for high-volume internal documents. Be honest about the tradeoff: we are buying lower resource cost with older CSS.
  • Serverless is a hard no. Arbitrary binaries are not hostable on Vapor-class runtimes. This is a pre-commitment constraint, not a post-implementation discovery — if the platform roadmap points at serverless, this package is disqualified today and we should say so plainly rather than defer it.

Failure modes. Enumerated with the operational response each implies:

Failure Signature Response
Binary missing from image Immediate RuntimeException from proc_open; smoke test catches it Image rollout gate; smoke test in deploy pipeline
Hung render Worker stalls until PHP max_execution_time or queue timeout, no useful error Set numeric timeouts. Shipped default is false — change it in week one. Numeric timeout produces a catchable exception we can surface as a real 500
Silent asset drop Invoice renders, logos and CSS missing, no error enable-local-file-access in the option profile
Option leakage between renders/jobs Two documents share settings; reproduces intermittently in workers forgetInstance('snappy.pdf.wrapper') convention, enforced
Wrong-language PDF Queued invoice in worker default locale App::setLocale() before render; cover with a test
Font fallback Everything shifts; reads as a margin bug Fonts in image; visual regression on font changes
SSRF / local file read None until exploited Input allowlist + HTMLPurifier; security review item
Broken/ragged page breaks Long tables split badly Chunk long tables; accept imperfect breaks for internal documents

The recovery story is good: failures are loud (RuntimeException carries the exact command and stderr, frequently self-explanatory) and reproducible outside PHP by running that command by hand. That is a materially better debugging experience than a misbehaving PDF library, and it should count in the package's favor.

Ramp-up. Low-to-moderate, with a clear split between what's fast and what isn't.

  • Fast (half a day): install, publish config, first PDF from a route. The API is small — loadView, loadHTML, loadFile, setOption(s), setPaper, setTimeout, inline, download, save, output, stream — and PdfWrapper.php is ~150 lines and readable in ten minutes. A mid-level Laravel developer is productive on the golden path in a day.
  • Slow (1–3 weeks to real fluency): print CSS. This is where the schedule risk lives, and it is not a skill the team currently has. The trap is applying modern frontend habits — Tailwind, Flexbox, Grid — and then debugging a decade-old WebKit. The guidance to give upfront: table layouts, ~60 lines of inline print CSS, and resist the utility framework. One team member should own document templates as a specialty rather than rotating the work.
  • Container/infra skills required: Dockerfile binary installation, font baking, verifying behavior as the FPM user. If we lack anyone comfortable with container image ownership, that gap is a bigger schedule risk than the PHP work.

Bottom line for the portfolio view. Adopt narrowly and deliberately: internal, high-volume, moderate-fidelity documents — invoices, reports, OG images — where the renderer seam and the exit ADR are in place from day one. Do not adopt for customer-facing brand-critical output, accessibility-regulated PDFs, modern-CSS-heavy layouts, or any roadmap pointing at serverless; use a Chrome-backed renderer for those. The 48/100 score is not an argument against the Laravel package, which is well-maintained and promptly tracks new Laravel releases — it is an accurate read on the aging engine underneath, and the right response is to capture that risk explicitly in an ADR with a named alternative and a revisit trigger, rather than to pretend the score reflects the wrapper.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle