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

Salvager Laravel Package

revolution/salvager

Revolution Salvager is a Laravel package for salvaging recoverable data from failed or interrupted operations, helping you capture what would otherwise be lost and restore it cleanly. Lightweight, configurable, and built to fit existing Laravel workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

Salvager is a thin Laravel wrapper around Playwright, aimed at the case where Http::get() isn't enough because the page you want is rendered client-side. It is a small, single-maintainer package (7 stars, no known dependents), so treat the source as the documentation.

Prerequisites (budget for these first — they dominate setup time):

  • PHP 8.1+ and a Laravel 9/10/11 app.
  • Node.js plus the Playwright browser binaries. PHP Playwright bindings generally drive a Node-side Playwright process, so Node is not optional even though your app is PHP.
  • A Linux/CI environment with the Chromium system dependencies (npx playwright install --with-deps chromium).

Minimal steps:

  1. composer require revolution/salvager
  2. Check for a publishable config (php artisan vendor:publish --tag=salvager-config or similar) and read config/salvager.php for browser path, headless flag, and timeout settings.
  3. Install the browser binary Playwright expects — usually npx playwright install chromium. Confirm the path Playwright reports matches what the config expects.
  4. Smoke-test outside Laravel first: run the package's own test suite or a one-line script that opens https://example.com and echoes the title. If that fails, the problem is Node/browsers, not your Laravel code.

First use case: a single JS-rendered page. Something like:

// Illustrative — confirm method names against the package source/README
$html = Salvager::browse('https://example.com/pricing')
    ->waitForSelector('.pricing-table')
    ->content();

Then graduate to a two-URL crawl before touching concurrency. The natural first real job is "fetch a listing page, extract detail-page links, fetch each detail page, store structured rows" — but do it sequentially first.

Where to look first: src/ (there is little of it), the service provider to see what's bound into the container, and the config file. The README will be terse; the class signatures are the real API reference.

Implementation Patterns

Wrap it in a queued job, always. Never call Salvager from a controller or a request cycle. A browser launch is 1–3 seconds of overhead and tens of megabytes of RAM.

class ScrapeListing implements ShouldQueue
{
    public int $tries = 3;
    public int $timeout = 120;
    public array $backoff = [30, 120];

    public function __construct(public string $url) {}

    public function handle(): void
    {
        $html = Salvager::browse($this->url)->content();
        // parse, persist, dispatch child jobs for detail pages
    }
}

One job per URL, with a visited-set. Keep a crawled_urls table (or a cache key per normalized URL) and guard at the top of the job. Deduplicate on a normalized form — strip fragments, sort query params — or you will re-crawl the same page dozens of times.

Separate "fetch" from "parse". Store raw HTML (or a screenshot) to storage/app/scrapes/{hash}.html and parse from that. When a selector breaks you can re-parse the archive instead of re-hitting the site. This is the single highest-value habit with browser-driven scraping.

Bound concurrency explicitly. Playwright is memory-hungry. If you run queue:work with 8 workers, you get 8 Chromium instances. Either run a dedicated worker pool for scraping with --max-jobs/low worker count, or use RateLimited/WithoutOverlapping middleware to serialize per host.

public function middleware(): array
{
    return [(new RateLimited('scraping'))->dontRelease()];
}

Integration points that pay off:

  • Dispatch from a scheduled command (Schedule::command('scrape:listings')->hourly()), not from user requests.
  • Persist into a DTO or a ScrapedPage model, so the rest of the app never imports the Salvager namespace. This is your escape hatch if the package is abandoned.
  • Feed results into Notification/mail or an export (maatwebsite/excel) — that's where the business value lands.
  • For auth-gated pages, set cookies/headers once at the context level and reuse the browser across a batch rather than relaunching per URL.

Testing: never let tests hit the real network. Point the crawler at a local fixture server (php -S on a fixture directory) serving saved HTML, or stub your own repository interface whose implementation wraps Salvager. Assert on parsing output, not on the crawler.

Gotchas and Tips

  • Node is a hidden dependency. Deploy scripts, Docker images, and CI pipelines all need Node + Chromium + shared libs. If you deploy to a slim container or serverless, verify this early — it is usually where these projects die.
  • Sandbox failures in Docker. Chromium needs --no-sandbox (or a properly configured user namespace) in most containers. If you see "Failed to launch browser" with no further detail, this is the first thing to check.
  • Always close the browser in a finally block. Aborted jobs leak Chromium processes; after a week you'll have hundreds. Monitor with ps aux | grep chrome after a deploy.
  • Sleeping is not waiting. Use waitForSelector/waitForLoadState rather than sleep(3). Fixed sleeps are simultaneously too slow and too short.
  • Timeouts cascade. There's typically a navigation timeout, a selector timeout, and the Laravel job timeout. Make sure job timeout > navigation + selector timeouts, or the worker is killed mid-scrape and retries forever.
  • Selectors are the real maintenance burden. Client-side pages change class names without warning. Prefer semantic selectors, assert on expected element counts, and alert when a scrape returns zero rows — a silent empty result is the most common failure mode and looks like success.
  • Respect the target. Set a realistic user-agent, honour robots.txt, add jitter between requests, and cache aggressively. Getting IP-blocked during development is a rite of passage; getting blocked in production is an incident.
  • Pin the version. With 7 stars and no dependents, there's little community pressure testing upgrades. Use an exact constraint ("revolution/salvager": "1.0.*") and read the diff before bumping.
  • Budget for the exit. The wrapper is tiny, so the abstraction cost of an interface is low and the payoff is high. Keep your own PageFetcher contract; swapping to symfony/panther, chrome-php/chrome, or Browsershot later becomes a one-class change.
  • If you only need simple JS rendering, consider a lighter fight. Spatie's crawler plus a Browsershot/Chromium render step, or calling a hosted rendering API, may be less operational surface than maintaining a Node Playwright sidecar — especially if you're scraping dozens of pages rather than thousands.
  • Debugging aids: run headed locally (headless: false), capture a screenshot on failure to storage/app/scrapes/failures/, and log the final URL after redirects — a surprising number of "selector not found" bugs are redirects to a consent or login page.
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.
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
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager