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

Chrome Laravel Package

chrome-php/chrome

Chrome PHP lets you control headless Chrome or Chromium from PHP, synchronously or asynchronously. Open pages, navigate, run JavaScript, take screenshots, generate PDFs, and emulate mouse and keyboard input with a simple, IDE-friendly API. Requires PHP 7.4–8.5 and Chrome/Chromium 65+.

View on GitHub
Deep Wiki
Context7

Getting Started

Three steps to first success:

  1. Install the package and a Chrome binary. The package is a DevTools-protocol client, not a browser.

    composer require chrome-php/chrome
    # one of:
    apt-get install -y chromium-browser    # Debian/Ubuntu
    # or run inside a chrome-enabled container image
    
  2. Point the factory at it. Either set CHROME_PATH in .env, or inject the binary explicitly:

    $factory = new \HeadlessChromium\BrowserFactory(env('CHROME_PATH', 'chromium-browser'));
    

    In Docker you will almost certainly also need noSandbox => true (see Gotchas).

  3. Run the smallest useful thing — usually a PDF, since that is the #1 day-to-day reason a Laravel app pulls this in:

    $browser = (new BrowserFactory())->createBrowser(['noSandbox' => true, 'windowSize' => [1280, 800]]);
    try {
        $page = $browser->createPage();
        $page->setHtml(view('invoices.pdf', $invoice)->render());
        $page->pdf(['printBackground' => true])->saveToFile(storage_path("app/invoices/{$invoice->id}.pdf"));
    } finally {
        $browser->close();
    }
    

Where to look first: in the README, the Available options table (that is your config surface), the Page API section (navigation, evaluate, pdf, screenshot), and the Page::* event constants for waitForNavigation. Then read BrowserFactory, Browser, and Page classes — the API is thin and IDE-friendly, so the source is often better documentation than the README.

First real use case: server-side PDF rendering of Blade views (invoices, reports, tickets) where you need CSS/JS fidelity that Dompdf and Snappy cannot give you. Screenshot capture and JS-heavy scraping are the next two.

Implementation Patterns

Bind a configured factory in a service provider. Keep options out of call sites and read them from config/services.php:

// AppServiceProvider::register()
$this->app->bind(BrowserFactory::class, fn () => tap(
    new BrowserFactory(config('chrome.binary')),
    fn ($f) => $f->setOptions(config('chrome.options'))
));

Use setOptions() / addOptions() when options should persist across every createBrowser() call; use the argument form when they should apply to a single browser only.

Run browser work in queued jobs, not in the request cycle. A cold Chrome start is 300–800 ms; heavy pages are seconds. Wrap the work in a job with $timeout, $tries, and always close() in finally:

public function handle(BrowserFactory $factory): void
{
    $browser = $factory->createBrowser();
    try {
        $page = $browser->createPage();
        $page->navigate($this->url)->waitForNavigation(Page::NETWORK_IDLE, 20000);
        $page->screenshot()->saveToFile($this->targetPath());
    } finally {
        $browser->close();
    }
}

Reuse one browser for multiple pages in a job. $browser->createPage() is cheap relative to browser startup. When rendering many invoices, create the browser once, loop pages, and $page->close() each one:

foreach ($invoices as $invoice) {
    $page = $browser->createPage();
    try {
        $page->setHtml(view('invoices.pdf', compact('invoice'))->render());
        $page->pdf(['printBackground' => true])->saveToFile($path);
    } finally {
        $page->close();
    }
}

Prefer NETWORK_IDLE for any page that loads data via XHR. LOAD fires before client-side rendered content is in the DOM; NETWORK_IDLE waits for a quiet network. Use DOM_CONTENT_LOADED when you only need markup, and Page::LOAD (default) otherwise.

Use evaluate() for scraping, callFunction() for structured extraction. callFunction passes arguments cleanly and returns serializable values, avoiding string-escaping bugs in interpolated JS:

$rows = $page->callFunction(
    'function () { return [...document.querySelectorAll("table tr")].map(r => r.innerText); }',
    []
)->getReturnValue();

Inject assets with addScriptTag / addPreScript. addPreScript runs before every navigation on that page — useful for stubbing navigator, intercepting fetch, or disabling analytics. setPagePreScript on the browser applies to every page it creates.

For a shared, long-lived browser (e.g. a worker process or several scripts on one box), use keepAlive => true, persist $browser->getSocketUri() to disk, and reconnect with BrowserFactory::connectToBrowser() inside a try/catch for BrowserConnectionFailed. This is not a pool — treat a persistent browser as single-tenant; do not let concurrent queue workers share one unless you isolate work by page and accept the blast radius of a crash.

Set userAgent, headers, proxyServer, ignoreCertificateErrors, and envVariables at the factory level rather than fighting the page API. These are browser-wide and consistently applied.

Testing: wrap the factory in a fake for unit tests, and reserve real browser calls for a small set of feature tests. Assert on the produced PDF/screenshot file size and mime type, not on byte equality.

Gotchas and Tips

  • Docker/CI will fail without noSandbox => true. The default sandbox needs user namespaces. Also add --disable-dev-shm-usage via customFlags if /dev/shm is small, otherwise Chrome crashes under load.
  • The binary must exist. BrowserFactory throws if it cannot find the executable. Pin CHROME_PATH explicitly in .env and config; do not rely on OS guessing.
  • keepAlive => true leaks Chrome processes if your script exits without calling close(). Either clean up explicitly, or schedule a reaper. Zombie Chromium eats RAM fast.
  • setHtml() replaces the document, it does not append. It also has its own timeout/event defaults (3000 ms / LOAD) that are separate from waitForNavigation.
  • waitForNavigation() defaults to a 30 s timeout on Page::LOAD. Always catch OperationTimedOut and NavigationExpired — the latter fires when a redirect replaces the document you were waiting on, which is common on auth flows and SPAs.
  • evaluate()->getReturnValue() is the blocking point, not evaluate(). If you navigate or click and then forget waitForPageReload(), you will read the old document.
  • Return values must be JSON-serializable. DOM nodes, functions, and cyclic structures come back as null or throw. Map to primitives in the JS.
  • Viewport vs window size are different. windowSize is the browser window; $page->setViewport(1280, 800) controls the rendering surface used for screenshots and layout. For deterministic PDFs, set the viewport and use pdf(['printBackground' => true]) — backgrounds are off by default and the omission is a classic "why is my invoice blank" bug.
  • Proxy authentication does not work (proxyServer does not accept credentials). Plan around it or put an authenticating proxy in front.
  • --enable-automation flag trips some sites' bot detection. Remove it via excludedSwitches => ['--enable-automation'] together with a realistic userAgent.
  • Debugging workflow: headless => false (needs a display on Linux), connectionDelay => 0.5, and debugLogger => 'php://stdout' or a PSR-3 logger. debugLogger accepts a stream path, a resource, or a LoggerInterface; wire it to Log::channel('chrome') in development only.
  • Queue timeouts must exceed browser timeouts. waitForNavigation(…, 20000) inside a job with a 15 s $timeout will be killed mid-flight, orphaning the Chrome process.
  • Version drift is real. The package supports Chrome 65+; keep the container image and the composer package aligned. Upgrading Chrome without running your PDF/screenshot tests is a common source of subtle layout regressions.
  • The factory option precedence is easy to get wrong: options passed to createBrowser() completely replace the factory defaults for that call; they are not merged.
  • Recycle browsers in long-running workers. Even with good cleanup, Chromium accumulates memory. Recreate every N pages (e.g. 50) or on a time budget rather than running one browser for hours.
  • Extension points are thin by design: addPreScript / setPagePreScript for page hooks, a custom LoggerInterface for observability, and customFlags for anything Chrome does that the package does not expose. If you find yourself needing more, you are probably writing raw DevTools protocol — consider whether chrome-php/chrome is still the right layer.
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.
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json