anam/phantommagick
PhantomMagick is a Laravel wrapper for PhantomJS that simplifies capturing screenshots and generating PDFs from web pages or HTML, with a fluent API for running PhantomJS scripts in PHP applications.
Install the wrapper:
composer require anam/phantommagick
Install PhantomJS — the package is only a wrapper; it shells out to the phantomjs binary. Grab the static build (2.1.1 is the last release), extract it, and put it on PATH (e.g. /usr/local/bin/phantomjs). On Debian/Ubuntu you also need libfontconfig1 and libfreetype6.
Laravel setup. On 5.5+ the PhantomMagickServiceProvider and PhantomMagick facade are auto-discovered. On older versions register them in config/app.php. Then publish the config:
php artisan vendor:publish --provider="Anam\PhantomMagick\PhantomMagickServiceProvider"
Configure the binary path — the single most important value in the whole package:
// config/phantommagick.php
'binary_path' => env('PHANTOMJS_PATH', '/usr/local/bin/phantomjs'),
First use case — an invoice or report rendered to PDF:
use PhantomMagick;
PhantomMagick::addPage(view('invoices.show', $invoice)->render())
->setOption('paperSize', 'A4')
->save(storage_path('app/invoices/inv-001.pdf'));
Where to look first: config/phantommagick.php, then Anam\PhantomMagick\Converter (a single small class that does all the work), then the knplabs/knp-snappy README — PhantomMagick is a thin Laravel layer over Snappy, so most option names (paperSize, orientation, marginTop, width, height) come straight from there.
Render Blade to HTML, don't concatenate strings. The everyday pattern is addPage(view(...)->render()). Keep a dedicated pdf/ view directory with print-specific CSS rather than reusing web layouts.
Two output modes. PDF via save() / download(); images via toImage():
PhantomMagick::addPage($html)
->setOption('width', '1024px')
->setOption('height', '768px')
->toImage()
->save(storage_path('app/thumbnails/shot.png'));
Store first, stream second. download('name.pdf') returns a Symfony response for the browser, but for user-triggered downloads it's more robust to save() to disk and serve with response()->download() — that way retries, caching, and range requests work.
Wrap it in a small service class. Don't sprinkle the facade through controllers. Inject a PdfGenerator that owns the binary path, default options, and storage location. That gives you one place to swap engines when you leave PhantomJS behind.
Always queue the conversion. Even trivial pages take 1–5 s and PhantomJS is a memory hog. Dispatch a GenerateInvoicePdf job, store the artifact, notify the user, and let the HTTP request return immediately.
Compose options centrally:
$converter->setOptions([
'paperSize' => 'A4',
'orientation' => 'portrait',
'marginTop' => '10mm',
'marginBottom' => '10mm',
]);
Multi-tenant / per-project paths: call setBinaryPath() at runtime from a config row rather than relying on the global config file — useful when different worker containers or environments have the binary in different locations.
PhantomJS is dead. It was archived in 2018 and gets no security patches. With 160 stars and zero dependents on this package, treat it as a stopgap and plan a migration to Browsershot/Puppeteer, wkhtmltopdf, or a Gotenberg sidecar.
"Could not execute phantomjs" is the #1 error — nearly always a wrong binary_path, a non-executable file, a missing libfontconfig, or a path that differs between your host and the Docker image. Verify with exec(config('phantommagick.binary_path').' --version') in tinker before blaming the package.
Modern CSS is unsupported. PhantomJS ships an old WebKit build: no CSS Grid, patchy Flexbox, partial ES6, no fetch. Lay documents out with tables, floats, and page-break-* rules. Test the actual output, not the browser preview.
Absolute URLs only. Remote CSS, images, and fonts are frequently dropped. Inline assets as data URIs or reference http:// URLs the server can reach — a relative /img/logo.png renders blank.
Fonts must exist on the server. If your laptop has Inter and production doesn't, you get a silent fallback. Embed via @font-face with a data URI, or pin fonts in your Dockerfile.
Async content needs a delay. Pages that populate via JavaScript capture blank unless you raise setDelay() / setTimeout(). The Snappy default is 60 s and PhantomJS will happily exceed it on heavy pages.
Temp files and permissions. Snappy writes to the system temp directory; open_basedir restrictions, noexec mounts on /tmp, or a read-only filesystem produce confusing "failed to create output" errors. Point TMPDIR at something writable in the container.
Config caching bakes in the binary path. After php artisan config:cache, an env('PHANTOMJS_PATH') change won't take effect until you rebuild. Keep the binary path in config, not in code.
download() must be the last call. Once headers are sent you'll get "headers already sent" — don't echo debug output before it.
Extension points: setBinaryPath() for runtime overrides, setOptions() for any Snappy CLI option, and the phantommagick config file for defaults. There's no event or macro hook, but the Converter class is small and unfinal, so it's easy to extend or decorate if you want auditing or metrics around conversions.
How can I help you explore Laravel packages today?