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

Php Phantomjs Laravel Package

jonnyw/php-phantomjs

PHP PhantomJS is a headless browser wrapper for PHP that lets you load pages, run JavaScript, capture screenshots, and export to PDF. It provides a clean, extensible API over the PhantomJS binary, supports custom scripts, and works with any PHP project.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install the library

composer require jonnyw/php-phantomjs

2. Install the PhantomJS binary separately. The package is only a wrapper — it shells out to a phantomjs executable that must exist on the machine.

# Ubuntu/Debian (if the package still resolves)
apt-get install phantomjs
# or grab the 2.1.1 static build
wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
tar -xjf phantomjs-2.1.1-linux-x86_64.tar.bz2 -C /opt
ln -s /opt/phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/local/bin/phantomjs
phantomjs --version   # sanity check

3. Smoke-test before touching Laravel.

use JonnyW\PhantomJs\Client;

$client = Client::getInstance();
$client->getEngine()->setPath('/usr/local/bin/phantomjs');   // binary path
$client->getEngine()->setCacheDir(storage_path('app/phantomjs')); // must be writable

$request = $client->getRequest();
$request->setUrl('https://example.com');
$request->setTimeout(5000);   // ms — hard process timeout
$request->setDelay(1000);     // ms — extra wait after load

$response = $client->send($request);

if ($response->getStatus() !== 200) {
    throw new RuntimeException('Phantom failed: '.$response->getStatus());
}

echo $response->getContent();

Where to look first (in this order):

File Why
src/Client.php Entry point; getRequest(), send(), lazy mode, debug
src/Engine.php Binary path, cache dir, CLI options, env vars
src/Http/Request.php, PdfRequest.php, CaptureRequest.php The three request flavours
src/Http/Response.php getContent(), getStatus(), getConsole(), getHeaders()
src/Resources/procedures/*.proc The actual PhantomJS scripts — read these to understand what the wrapper does

First real use case: generating a PDF of an existing Blade-rendered invoice route. It's the lowest-risk, highest-value application and exercises 90% of the package.

use JonnyW\PhantomJs\Http\PdfRequest;

$request = new PdfRequest();
$request->setUrl(route('invoices.show', $invoice));
$request->setFormat('A4');
$request->setOrientation('portrait');
$request->setMargin('10px');
$request->setTimeout(10000);
$request->setDelay(2000);

$response = $client->send($request);
Storage::put("invoices/{$invoice->id}.pdf", $response->getContent());

Implementation Patterns

Container binding + config (don't use Client::getInstance() in app code)

getInstance() is a static singleton — it makes mocking impossible and leaks state across requests in Octane/queue workers. Bind a configured instance instead.

// config/phantomjs.php
return [
    'binary'     => env('PHANTOMJS_BIN', '/usr/local/bin/phantomjs'),
    'cache_dir'  => storage_path('app/phantomjs'),
    'timeout'    => 10000,
    'delay'      => 1500,
    'options'    => [
        '--load-images=false',
        '--ignore-ssl-errors=true',
        '--ssl-protocol=any',
    ],
];
// AppServiceProvider::register()
$this->app->singleton(\JonnyW\PhantomJs\Client::class, function () {
    $client = \JonnyW\PhantomJs\Client::getInstance();

    $client->getEngine()->setPath(config('phantomjs.binary'));
    $client->getEngine()->setCacheDir(config('phantomjs.cache_dir'));

    foreach (config('phantomjs.options') as $option) {
        $client->getEngine()->addOption($option);
    }

    return $client;
});

Wrap it in your own PdfRenderer / PageScraper class so the rest of your app never imports JonnyW\* directly. That's what makes the eventual migration to Browsershot/Chrome cheap.

Rendering assets correctly

PhantomJS has no access to your APP_URL unless you tell it. For a PDF of a local page, either bake absolute URLs into the Blade template (asset(), url()) or force the root URL:

URL::forceRootUrl(config('app.url'));

Also send an auth cookie/header if the route is protected — the request carries no session:

$request->addHeader('Cookie', 'session='.encrypt($sessionId));

POST + JSON APIs (headless scraping)

$request->setMethod('POST');
$request->setUrl('https://api.example.com/search');
$request->setHeaders(['Accept' => 'application/json']);
$request->setContentType('application/json');
$request->setBody(json_encode(['q' => 'laravel']));

PhantomJS renders the page; getContent() gives you the post-JS DOM, getPlainText() gives you stripped text.

Screenshots

use JonnyW\PhantomJs\Http\CaptureRequest;

$request = new CaptureRequest();
$request->setUrl($url);
$request->setWidth(1280);
$request->setHeight(800);
$request->setCaptureType('png');
$request->setTimeout(10000);
$request->setDelay(2000);

$png = $client->send($request)->getContent();
Storage::disk('public')->put("shots/{$id}.png", $png);

Queue the work, never inline

Every send() spawns a PhantomJS child process (~300–800 ms cold, more for JS-heavy pages). PDF generation on a web request will destroy your p95. Push it into a job:

class GenerateInvoicePdf implements ShouldQueue
{
    public $timeout = 60;
    public $tries = 3;

    public function __construct(public int $invoiceId) {}

    public function handle(PdfRenderer $renderer): void
    {
        $pdf = $renderer->pdf(route('invoices.show', $this->invoiceId));

        Storage::put("invoices/{$this->invoiceId}.pdf", $pdf);
        Invoice::whereKey($this->invoiceId)->update(['pdf_generated_at' => now()]);
    }
}

Concurrency = number of queue workers; PhantomJS itself is single-page-per-process. Scale workers, not threads.

Custom procedures (the real extension point)

When you need to run JS in the page (scroll to bottom, click a "load more", read window.__DATA__), write a .proc file. It's a Twig-rendered PhantomJS script:

{# resources/phantomjs/extract.proc #}
var page = require('webpage').create();
page.viewportSize = { width: {{ width|default(1280) }}, height: 800 };

page.onConsoleMessage = function (msg) { console.log(msg); };

page.open('{{ url }}', function (status) {
    var data = page.evaluate(function () {
        return JSON.stringify(window.__INITIAL_STATE__ || {});
    });
    console.log(data);
    phantom.exit();
});

Register it, then read the output from $response->getConsole():

$client->getProcedureLoader()->addLoader(
    new \JonnyW\PhantomJs\Procedure\FileLoader(resource_path('phantomjs'))
);

$request->setProcedure('extract');   // matches extract.proc

Gotchas and Tips

This package and PhantomJS are both archived (last release 2018; PhantomJS died in 2018). Treat it as legacy maintenance, not a greenfield choice. It depends on old Symfony components and Twig — on PHP 8.x you will hit deprecations, and some builds fatal outright. If you're starting fresh, spatie/browsershot (headless Chrome) or chrome-php/chrome solve the same problems with a living upstream. If you inherit this package, isolate it behind your own interface so swapping the driver is a one-file change.

The binary is not bundled. InvalidExecutableException at runtime almost always means setPath() points at a directory instead of the executable, or the binary isn't on the box at all (Docker images frequently omit it). Pin the version — behaviour differs between 1.9 and 2.1.1.

proc_open is the transport layer. Shared hosts and hardened PHP-FPM configs routinely disable it in disable_functions. You'll get silent failures, not a clear error.

The cache dir must exist, be writable, and be cleaned. Compiled .proc files are cached there. Edit a procedure, forget to clear the cache, and you'll debug the wrong file for an hour. Also: screenshots and cookies land there, so prune it or it grows unbounded. Use storage/app/phantomjs — not /tmp in a container that gets recycled mid-flight.

setTimeout() and setDelay() are both in milliseconds and mean different things. timeout is the hard process kill; delay is a fixed sleep after page.open returns. For any page with client-side rendering, bump delay to 1500–3000 ms — a "blank PDF" is nearly always a race, not a bug in your template. There's no waitForSelector in the default procedure; if you need real synchronisation, write a custom .proc.

Modern sites will break PhantomJS, and it won't be your fault. PhantomJS 2.1.1 predates ubiquitous ES6, fetch, Promise, and modern TLS defaults. Expect SyntaxError on React/Vue bundles and TLS handshake failures against SNI-only hosts. The escape hatches (--ssl-protocol=any, --ignore-ssl-errors=true) work for the TLS side; there is no fix for the JS side other than transpiling the target page or switching tools.

getStatus() !== 200 covers a lot of ground. It returns the PhantomJS page status, which can be null/0 when the process died (segfault, OOM, binary missing). Always log $response->getConsole() and the engine stderr on failure — that's where real errors surface.

getContent() is binary for PDF and capture requests. Never return response()->json(['pdf' => $response->getContent()]) — you'll corrupt the bytes. Stream it (response()->streamDownload()) or write to storage and hand back a signed URL. Add --load-images=false when generating text PDFs; it's often a 2–3× speedup.

Client::getInstance() is a singleton with mutable state. In a queue worker that processes PDFs configured for A4 and screenshots configured for 1280×800, options set once leak into later jobs. Rebuild the client per job, or resolve it fresh from the container with bind rather than singleton if you vary config.

Testing: mock at your own wrapper boundary, not at Client. There's no built-in fake; attempting to unit-test the procedure compiler is rarely worth it. Assert that your job writes a non-empty file, and keep a single integration test that hits a local Blade route.

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.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
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