terminal42/escargot
A Symfony HttpClient-based crawler framework for PHP that can crawl any HTTP resource, not just websites. Jobs are tracked by ID and backed by a queue, so long crawls can pause and resume.
ExceptionSubscriberInterface::onTransportException() now receives ResponseInterface|null, so the pattern is unchanged but every implementation of that interface is touched.redirected_to), so any custom/Laravel-native queue adapter is a first-class integration surface, not an afterthought.CrawlUri::getRedirectedTo() exposes the final destination while getUri() retains the originally requested URI — useful for canonical URL selection, sitemap correctness, and de-duplication.Http facade is built on Symfony HttpClient, so Escargot’s core functionality integrates seamlessly.InMemoryQueue or DoctrineQueue with effort — but under 2.0 a replacement adapter must persist and restore redirect information, or redirect semantics silently degrade.DoctrineQueue works with Laravel’s Eloquent/Query Builder, but a custom queue adapter could be built for Laravel’s database connection. 2.0 requires a schema change (nullable redirected_to) on any existing queue table before the new version is usable.ExceptionSubscriberInterface::onTransportException() now accepts ResponseInterface|null. Existing implementations with the old narrower type are contravariance violations and will fail at class load / first resolution — i.e., a deploy-time fatal, not a graceful runtime error. Mitigation: audit and update every subscriber implementation in the same release; grep the codebase for onTransportException and ExceptionSubscriberInterface.DoctrineQueue requires a nullable redirected_to column. Deploying 2.0 before migrating means runtime SQL errors mid-crawl, which is the worst possible failure point for a long-running job. Mitigation: schema migration must precede the package bump in the deploy sequence, and any custom persistent queue must be updated to store/restore redirects.getUri() retains the original requested URI; getRedirectedTo() exposes the final destination. Any subscriber, dedupe key, or sitemap/export that previously assumed a single "current URI" must be re-validated. Expect different crawl graphs and different stored URLs after upgrade.InMemoryQueue, crawls won’t survive process restarts. Mitigation: Use DoctrineQueue or a Redis-backed queue in Laravel (both now redirect-aware).Symfony HttpClient’s delay()).HtmlCrawlerSubscriber may require additional libraries (e.g., symfony/dom) for robust HTML processing. Laravel’s str or html helpers could supplement this.InMemoryQueue) or database locks (DoctrineQueue) could become bottlenecks. Mitigation: Use LazyQueue with a fast primary queue (e.g., Redis) and persistent fallback (e.g., database) — and confirm both layers carry redirected_to, otherwise LazyQueue fallback will lose redirect state on recovery.HtmlCrawlerSubscriber vs. custom JSON parsers) — and, post-2.0, whether redirect destinations must be surfaced in output.ExceptionSubscriberInterface implementations? Who owns each, and can they all ship in the same release window as the 2.0 bump?DoctrineQueue or a Laravel queue (e.g., Redis) is mandatory — and under 2.0, redirect persistence is a hard precondition, not optional.getUri(), getRedirectedTo(), or both, and whether existing records need backfilling.null response) be retried? The null case is exactly the bug 2.0 fixes — confirm our subscribers handle "no response object" as a legitimate state rather than an anomaly.Http facade for consistency.Escargot and its dependencies (queues, subscribers) as Laravel services.database, redis) as the primary queue, with LazyQueue for fallback persistence — verifying redirect state survives both paths.EscargotCommand to trigger crawls via php artisan escargot:crawl.DoctrineQueue table, including the new nullable redirected_to column.jobs table or a custom table.ExceptionSubscriberInterface and all queue adapters. Confirm each can be updated for the new ResponseInterface|null signature and redirect persistence.composer require terminal42/escargot:^2.0 symfony/http-client symfony/dom
// app/Providers/EscargotServiceProvider.php
public function register()
{
$this->app->singleton(Escargot::class, function ($app) {
$queue = new LazyQueue(
new RedisQueue(), // Primary: Laravel Redis queue
new DoctrineQueue($app['db']) // Fallback: Laravel DB (must persist redirected_to)
);
return Escargot::createFromJobId(
$app['config']['escargot.job_id'],
$queue
);
});
}
App\Subscribers\DataExtractorSubscriber) implementing SubscriberInterface.null response (validation check, below):
// Verify the exact parameter order/types against the installed vendor interface.
public function onTransportException(
CrawlUri $crawlUri,
ResponseInterface|null $response,
\Throwable $exception
): void {
if ($response === null) {
// Failure occurred before a response existed (connection reset, DNS, TLS).
// Retry/backoff logic must live here rather than in response inspection.
}
}
$escargot = $this->app->make(Escargot::class);
$escargot->addSubscriber(new RobotsSubscriber());
$escargot->addSubscriber(new HtmlCrawlerSubscriber());
$escargot->addSubscriber(new DataExtractorSubscriber());
// database/migrations/xxxx_add_redirected_to_to_escargot_queue.php
public function up(): void
{
Schema::table('escargot_queue', function (Blueprint $table) {
$table->string('redirected_to')->nullable();
});
}
store/restore logic to carry redirected_to, then write a round-trip test.// app/Console/Commands/EscargotCrawl.php
public function handle()
{
$escargot = app(Escargot::class);
$escargot->crawl();
}
app/Console/Kernel.php):
protected function schedule(Schedule $schedule)
{
$schedule->command('escargot:crawl')->dailyAt('3:00');
}
.env:
QUEUE_CONNECTION=redis
LazyQueue to combine speed (Redis) and persistence (database):
$queue = new LazyQueue(
new RedisQueue(), // Fast primary queue
new DoctrineQueue($app['db']) // Persistent fallback (redirect-aware)
);
HttpClient, DomCrawler) ensure seamless integration. The 2.0 exception change tracks Symfony’s own behavior, where transport failures can occur before a response exists — so the new contract is more compatible with Laravel’s HTTP stack, not less.DoctrineQueue works with Laravel’s Eloquent or Query Builder, but a custom adapter could use Laravel’s DB facade directly — it must implement the new redirect contract.CrawlStarted, UriProcessed, UriRedirected) for cross-service communication. Adding a redirect-aware event is low cost now that getRedirectedTo() exists.^1.7 and ^2.0 are not interchangeable at the interface or schema level. Treat the bump as a coordinated release, not a routine patch.RobotsSubscriber and HtmlCrawlerSubscriber.InMemoryQueue for validation (accepting no restart resilience).redirected_to migration ahead of the package bump.InMemoryQueue with DoctrineQueue or a Laravel queue adapter that persists redirects.ResponseInterface|null signature.getRedirectedTo(), rate limiting).LazyQueue with Redis + database), confirming redirect state survives failover.dispatch() crawls as jobs).HttpClient). Laravel’s built-in Symfony packages will handle most updates, but custom subscribers may need adjustments.composer update. Keep a pinned ^1.7 branch available for rollback until the new crawl output is validated.ResponseInterface|null contract is likely to persist, so it is a one-time cost.php artisan queue:work) will handle Escargot’s queue processing. Supervisor or Horizon can manage worker processes.DoctrineQueue, ensure the database is optimized (e.g., indexes on job_id, and on redirected_to only if queried). Redirect values are unbounded strings — consider a prefix/length policy if the queue table grows large.LazyQueue failover can replay entries.getUri() and getRedirectedTo() to keep historical data consistent across the upgrade boundary.@Toflar). Major releases are infrequent but contract-breaking; subscribe to releases and review changelogs before any composer update in CI.getUri() is now explicitly the requested URI.redirected_to column → SQL error mid-crawl, partial progress, potentially a stuck job. Detection: crawl aborts with a database exception, not an HTTP error. Mitigation: migration-before-bump deploy ordering.null response unhandled → the original 2.0 bug class. Subscribers must not assume $response->getStatusCode() exists; a swallowed null path can silently drop retries and under-report failures.getRedirectedTo() survives store/restore.LazyQueue fallback divergence → crawls resume with partial redirect knowledge after a primary-queue failure. Detection: intermittent, non-reproducible URL anomalies.ExceptionSubscriberInterface signature violations, (3) redirect state survives queue failover, (4) crawl output diffed against the pre-upgrade baseline and explained.How can I help you explore Laravel packages today?