spatie/crawler
A powerful PHP package for crawling websites, using Guzzle promises to fetch multiple URLs concurrently. It can render JavaScript via Chrome and Puppeteer, restrict crawls to internal links or depth, collect found URLs, and fake HTTP responses for testing crawl logic.
Architecture fit. spatie/crawler is a standalone, framework-agnostic PHP library, not a Laravel package in the conventional sense: there is no service provider, facade, config file, or publishable migration. That is a feature, not a gap — it means the crawl engine composes cleanly into a Laravel app as a library invoked from an Artisan command, a queued Job, or a scheduled task, with no bootstrapping ceremony and no coupling to the framework's container. For a Laravel/PHP stack the fit is high: it is same-vendor as much of the Spatie ecosystem already in use, MIT-licensed, Composer-installable, and its extension points (observers, custom UrlParser, custom CrawlQueue, JavaScriptRenderer) are interface-based and injectable.
Integration feasibility. Straightforward. The API surface is fluent and small (Crawler::create()->internalOnly()->depth()->onCrawled()->start()), and the package ships two capabilities that materially de-risk adoption:
->fake([...]) allows deterministic unit tests of crawl logic with zero HTTP, which fits existing PHPUnit/Pest pipelines.->shouldStopCallback() plus the FinishReason enum and CrawlProgress counters give us external stop control and terminal-state reporting — the two things missing from most crawler libraries and the usual cause of unkillable long-running jobs.The main integration seam to design is persistence: the library emits results to observer callbacks and returns CrawledUrl objects; it does not store anything. Our value-add is the observer implementation that writes to our models/queue.
Technical risk.
CrawlQueue.spatie/browsershot is now a suggest (v9), not a requirement. Enabling it means Node, Puppeteer, and a headless Chrome binary in the runtime image — a significant infrastructure and image-size commitment. Default to off; treat as an explicit opt-in decision.UriInterface, CrawlResponse instead of PSR-7, CrawlProfile became an interface, startCrawling() removed). A large fraction of tutorials and Stack Overflow answers target v8. Pin the major and treat the UPGRADING guide as required reading.Key questions before committing.
Stack fit. Native Laravel/PHP fit with a deliberately thin footprint. Requirements are PHP 8.x and Guzzle 7 or 8 (v9.4.0+ added Guzzle 8 support, v9.4.1 fixed Guzzle 8 exception handling — pin ^9.4.1 at minimum). No Laravel version lock in v9; the package does not depend on illuminate/* at all, so Laravel 11/12/13 compatibility is a non-issue, which removes the usual upgrade-treadmill worry. Existing Spatie tooling in the codebase reduces vendor sprawl and review friction.
Migration path. Greenfield — nothing to migrate. The only migration-shaped risk is a future major bump from 9.x to 10.x, which given the project's history (v9 was a full rewrite) will likely be breaking. Mitigate by isolating the package behind a thin internal service class (SiteCrawler) that owns all direct Crawler:: calls; swapping majors then touches one file. Do not let Spatie\Crawler\* types leak into controllers, models, or domain services.
Compatibility. Two friction points with the surrounding stack:
Http client. Existing global HTTP middleware, retry policy, and Http::fake() in tests will not apply. Use the package's fake() for crawler tests and keep Guzzle-level configuration (->middleware(), ->retry(), ->proxy()) explicitly in the wrapper.Sequencing. Recommend a four-stage ramp, each stage independently shippable:
foundUrls(); validate depth, internalOnly() + includeSubdomains() + matchWww() scoping, and robots.txt behavior. Confirm the output shape we actually need.->limit(), ->depth(), ->delay(), ->userAgent(), and a shouldStopCallback reading a stop flag (cache/DB) so an operator can halt a runaway crawl. Emit FinishReason and CrawlProgress to logs. This is the minimum viable production surface.crawl_runs / crawled_urls tables with batched inserts; schedule via Laravel's scheduler with overlap protection (withoutOverlapping()). Add diffing against the prior run for link-integrity and redirect-drift reporting.CrawlQueue (Redis/DB) and shard by hostname across workers. Defer until stage 3 data proves the need — the package's single-process model may well suffice.Maintenance. Favourable profile: actively maintained by Spatie with a documented changelog, tagged releases, and a steady cadence of small patch releases (v9.3.x–v9.4.2). Recent entries are reassuringly boring — sitemap namespace parsing, Guzzle 8 exception handling, skipping pcntl signal registration when those functions are disabled, closing response body streams after processing. The last item matters operationally: it is an explicit memory-leak fix, evidence the maintainers are tending production concerns. Budget for routine minor upgrades each quarter; budget a real project for any major.
Support. Community-only via GitHub issues, no SLA, no vendor contract implied by the MIT license. For a non-critical internal tool that is fine. If crawl output becomes a customer-facing feature or a compliance control, the lack of commercial support must be explicitly accepted by someone with authority, or backstopped by an internal owner who can read the source — which is small and readable.
Scaling. The honest constraint: one crawl = one PHP process, concurrency inside that process via Guzzle promises, visited-URL state in memory unless replaced. Vertical scaling (more memory, more concurrency, tuned delay()) is easy; horizontal scaling requires a custom CrawlQueue and a partitioning strategy, which is real engineering we would own entirely. Practical guidance: for dozens of sites at shallow depth on a nightly cadence, a single worker suffices; for thousands of domains or deep crawls, plan for the persistent-queue workstream up front rather than discovering it under load.
Failure modes to instrument.
->limit() caps and alert on process RSS.->limit(), ->depth(), an execution time limit, and the stop-flag callback; map FinishReason to distinct alert severities (CrawlLimitReached/TimeLimitReached are warnings, not successes).pcntl is often unavailable in containerized or restricted environments (v9.3.2 patched a crash here). Verify signal behavior in the actual runtime image, not on a dev machine.AdaptiveThrottle and retry() exist; use a descriptive, contactable userAgent() and honour robots.txt.redirectHistory()/wasRedirected() make these observable; cap redirects.crawlFailed from empty results in reporting.urlsFound vs urlsCrawled per run and alarm on significant divergence from the trailing baseline.Ramp-up. Low for any mid-level Laravel developer — the fluent API and fake() testing hook mean a productive first command inside a day, production-grade integration inside a sprint. The genuinely non-obvious knowledge is concentrated in three places: the v9 breaking-change surface (docs lag), memory/state semantics of the default queue, and safe observer design. Capture those three in an ADR and a runbook and the bus factor is manageable.
How can I help you explore Laravel packages today?