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+.
Architecture fit. chrome-php/chrome is a framework-agnostic PHP client that drives a real Chrome/Chromium binary over the DevTools Protocol, communicating with the browser process via a WebSocket. That means the package is not a rendering library — it is a process orchestrator plus protocol client. In a Laravel application it occupies the same architectural slot as a headless-browser microservice would, except the browser runs as a child process on the same host as the PHP worker. For teams already running PHP workers, this removes a network hop and a service to deploy; for teams running ephemeral, autoscaled containers, it adds a heavyweight native dependency (a ~300 MB browser binary) to every runtime image that needs rendering. The synchronous-looking API (getReturnValue(), waitForNavigation()) is actually promise-based under the hood, so the "async" claim is real but only useful inside a long-running process that manages its own event loop — not inside a typical FPM request. Architecturally, this package wants to live in queue workers, not web requests.
Integration feasibility. Composer install is trivial (composer require chrome-php/chrome), and the API surface is small and IDE-friendly: BrowserFactory → Browser → Page → navigation/evaluate/screenshot/pdf. There is no Laravel service provider, config file, or facade, so integration is a thin wrapper you write yourself. The real feasibility question is environmental: the package needs CHROME_PATH or a discoverable chrome/chromium-browser binary, a writable temp dir for the generated userDataDir, and enough shared memory (/dev/shm) to avoid Chrome crashing. PHP 7.4–8.5 and Chrome 65+ are supported, but the README is explicit that only Linux is tested — macOS/Windows are "compatible," not validated. That is acceptable for server-side Laravel but a caveat for developer laptops.
Technical risk. The dominant risks are operational rather than code-level. First, blocking semantics: a synchronous navigate()->waitForNavigation() call can hold a PHP-FPM worker for 30 seconds by default; without queue isolation this is a self-inflicted availability incident. Second, external binary coupling: Chrome auto-updates, and DevTools Protocol surface changes across major versions, so a browser upgrade can break rendering in production without a Composer change. Third, security surface: this package will happily fetch internal URLs, file:// paths, and credentialed endpoints — a user-supplied URL parameter reaching this library is a server-side request forgery primitive and a potential data-exfiltration vector. Fourth, process hygiene: orphaned Chrome processes, zombie children, and accumulating temp profile directories are the classic failure signature. Fifth, maturity signal: README/CHANGELOG/releases exist and the project is actively released (most recent 2026-07-06, MIT), but the reported "Dependents: 0" means downstream adoption cannot be used as a proxy for production hardening — the 2.6k stars and the changelog are the better evidence.
Key questions.
/dev/shm sized, and is noSandbox acceptable under our container security policy?Stack fit. Good fit for a Laravel monolith that already has a queue and workers. The natural shape is a RenderPageJob dispatched to a dedicated queue, a thin BrowserManager singleton that holds a BrowserFactory with persistent options (windowSize, noSandbox, userAgent, proxyServer, customFlags), and a PageRenderer service that owns navigation, waiting, and artifact storage. Keep the package behind your own interface (HtmlRenderer, PdfRenderer, ScreenshotRenderer) so the vendor is swappable. Do not let HeadlessChromium\* classes leak into controllers or domain code.
Migration path. If the current solution is wkhtmltopdf, dompdf, or a Puppeteer/Playwright microservice, the migration is incremental and low-drama because the output contract (bytes or a file path) is identical. Sequence it as: (1) stand up a RenderPageJob on the existing queue with the package wired to a single route behind a feature flag; (2) diff outputs against the incumbent for a sample of real documents and pages; (3) move traffic by route or tenant; (4) decommission the old path only after a full billing/reporting cycle has run clean. If the incumbent is a separate Puppeteer service, be honest about the trade: you are trading a network boundary and a language boundary for a native binary inside your PHP image. That is a win on latency and operational count, a loss on blast radius and independent scaling.
Compatibility. Pin the package minor version in composer.json and pin the Chrome/Chromium version in the Docker image — never let both float. Verify PHP 8.x against the Laravel version in use and confirm the queue worker's memory limit (memory_limit) comfortably exceeds a browser session's footprint. Confirm the runtime has the fonts and shared libraries Chrome needs (libnss3, libatk, fontconfig, plus CJK/emoji font packages if document fidelity matters). If Redis/Horizon is the queue backend, set retry_after above the worst-case render timeout or jobs will be double-dispatched.
Sequencing.
sendSyncDefaultTimeout and startupTimeout, structured logging via a PSR-3 debugLogger, and guaranteed $browser->close() in finally.OperationTimedOut, NavigationExpired, browser crash), orphan process count, and temp-dir growth.Maintenance. Two things version independently and both must be maintained deliberately: the Composer package and the Chrome binary. Budget a recurring task for browser-image refresh plus a smoke-render test in CI that fails loudly if Chrome upgrades break navigation or PDF output. The package itself is MIT and actively released, so security patches should be absorbable as routine minor bumps, but each bump deserves a changelog read — the changelog and releases are the primary support contract here.
Support. There is no commercial support and no vendor SLA; support is GitHub issues plus the README. That is normal for this class of package and is acceptable for internal tooling, less so for a customer-facing, revenue-path feature. Plan for at least one engineer to become the named owner of "browser rendering" — this is not a package you install and forget.
Scaling. Scale horizontally by adding worker processes, not by adding concurrency inside a process. The practical unit is one browser instance per worker with a small number of pages per browser, recycled after N jobs to bound memory growth. Because Chrome is CPU- and memory-heavy, the render queue should be isolated from the transactional queue so a render backlog cannot starve order processing — this is the single most important scaling decision. Persistent-browser mode (keepAlive plus BrowserFactory::connectToBrowser with a socket URI file) is available and can amortize startup cost substantially, but it introduces shared-state and stale-socket handling that must be designed for, not bolted on.
Failure modes to instrument explicitly.
OperationTimedOut / NavigationExpired — slow or redirected pages; needs per-job timeout budgets and graceful job failure, not worker death./dev/shm exhaustion, sandbox violations; distinguish "never started" from "crashed mid-render."memory_limit even though Chrome does the work.navigate() is the highest-severity failure mode in this list; enforce an allowlist and network egress policy before the first production job.Ramp-up. Expect roughly one to two engineer-weeks for a competent Laravel developer to reach a hardened first version: a day on the API and options, a day or two on the container and Chrome dependencies, the rest on the wrapper, timeouts, observability, and failure handling. The package's learning curve is genuinely low; the operational learning curve — Chrome in containers, process lifecycle, and queue isolation — is where the time actually goes. Plan the onboarding around the operational model, not the API.
How can I help you explore Laravel packages today?