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

Hquery Laravel Package

duzun/hquery

hQuery is a fast, lightweight PHP HTML/XML parser with a jQuery-like API. It supports CSS selectors and DOM traversal, has no external dependencies, and handles broken or incomplete HTML gracefully, making web scraping, content extraction, and HTML manipulation simple and memory-efficient.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer and you're done — no extensions, no PECL, no cURL requirement:

composer require duzun/hquery

The entry points you'll reach for first are the static factories:

  • hQuery::fromHTML($html) — parse a string you already fetched (best for tests and queued jobs)
  • hQuery::fromUrl($url, $options) — fetch + parse in one call (uses file_get_contents under the hood)
  • hQuery::fromFile($path) — parse a local file (useful for cached crawls or fixtures)

Your first use case: pull <h1> and link targets off a page.

use duzun\hQuery;

$doc = hQuery::fromHTML($html);
$title = $doc->find('h1')->text();
$links = $doc->find('a')->attr('href'); // array of hrefs

Then iterate nodes when you need structure rather than just values:

foreach ($doc->find('article.post') as $post) {
    $row = [
        'title' => $post->find('h2')->text(),
        'url'   => $post->find('a.more')->attr('href'),
        'body'  => $post->find('.excerpt')->html(),
    ];
}

Read the README's selector list first — hQuery supports CSS2 + most of CSS3 (#id, .class, [attr^="…"], :nth-of-type(), :not(), combinators), but it is explicitly not a browser engine.

Implementation Patterns

Separate fetching from parsing. hQuery::fromUrl() is convenient but it's file_get_contents — no retries, no cookie jar, no Guzzle middleware. In Laravel you almost always want to fetch with the Http facade or Guzzle and hand the body to hQuery::fromHTML():

$html = Http::withHeaders(['User-Agent' => config('scraper.ua')])
            ->timeout(15)
            ->get($url)
            ->throw()
            ->body();

$doc = hQuery::fromHTML($html, $url); // second arg = base URL for relative links

Wrap parsing in a dedicated service or DTO. Scrapers rot; keeping selectors in one class per site makes maintenance cheap.

final class ProductPageParser
{
    public function parse(string $html): ProductData
    {
        $doc = hQuery::fromHTML($html);
        return new ProductData(
            title: trim($doc->find('h1.product-title')->text()),
            price: (float) preg_replace('/[^\d.]/', '', $doc->find('.price')->text()),
            images: $doc->find('img.gallery')->attr('src'),
        );
    }
}

Cache aggressively. Site HTML rarely changes second-to-second; Laravel's cache is a natural fit:

$html = Cache::remember("html:$url", now()->addHours(6), fn () => Http::get($url)->body());
$doc  = hQuery::fromHTML($html);

Queue it with rate limiting. Scraping belongs in a ShouldQueue job so you can use RateLimiter::attempt(), Bus::batch(), and retryUntil(). Keep one job per URL (or pagination page) and persist parsed DTOs to the DB rather than the raw tree.

Pagination loop. Scrapers commonly need ?page=N — walk $doc->find('a.next')->attr('href') (or ->attr('href')[0]) until empty, with a hard cap to avoid infinite loops.

Tree traversal over regex. Use ->parent(), ->children(), ->next(), ->prev(), ->closest() to move around; it's far more robust than string munging and faster than DOMDocument for multi-megabyte inputs.

Testing. Fixture HTML + hQuery::fromHTML(file_get_contents(base_path('tests/fixtures/product.html'))) is a clean unit test with zero network. Pair with Http::fake() for integration coverage.

Gotchas and Tips

  • attr() and text() return arrays on collections. When you find() a set, ->attr('href') gives you ['/a', '/b']; ->text() behaves similarly. Indexing (->attr('href')[0]) or iterating nodes is the safe move. On a Node (obtained via foreach or ->get(0)) these return scalars.
  • Relative URLs stay relative. attr('href') does not resolve ../foo. Pass the base URL as the second argument to fromHTML($html, $baseUrl) and use hQuery::abs_url($href, $baseUrl) (or Guzzle's UriResolver) — otherwise you'll be crawling broken links.
  • No JavaScript. SPAs, lazy-loaded grids, and data-src images will be invisible. If content requires JS, this is the wrong package; reach for a headless browser.
  • Encoding matters. hQuery parses bytes, so UTF-8 is assumed. Sites serving ISO-8859-1 or Shift-JIS will produce mojibake; sniff the Content-Type/meta charset and run mb_convert_encoding($html, 'UTF-8', $charset) before parsing.
  • ->html() is innerHTML, not outerHTML. Casting a Node to string ((string) $node) yields the full tag if you need the outer markup for storage.
  • Selector dialect is limited. :has(), :nth-last-child(), and some comma-nested expressions won't behave like jQuery/browser CSS. Test selectors against real fixtures before shipping; keep them simple.
  • Empty results are collections, not nulls. Guard with if (count($doc->find('.x'))) or check for empty arrays — there's no null to fall back on, which is actually convenient but easy to forget.
  • Global static config. hQuery::$cache_path and hQuery::$cache_expires control the built-in file cache, and other statics affect parsing. In Octane or long-running workers, reset anything you mutate between requests to avoid config bleed.
  • Memory on huge documents. The tree lives in memory until the object is released. unset($doc) after extracting data in long queue workers — otherwise memory creeps across thousands of jobs.
  • Extension point: subclass it. You can extend hQuery to add helpers (e.g. ->absLinks(), ->textTrimmed()), and it's a cleaner place to centralize quirks than sprinkling trim() calls through your code.
  • fromUrl() defaults are thin. It doesn't gzip-decode reliably and doesn't retry. If you must use it, pass ['User-Agent' => ..., 'timeout' => 10] and treat failures as expected, not exceptional.
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.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings