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

Phpquery Laravel Package

electrolinux/phpquery

phpQuery is a server-side, jQuery-like PHP library for parsing and manipulating HTML/XML DOM. It provides CSS3 selectors, chainable traversal and manipulation methods, and a familiar API for scraping, templating, and document processing.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm it loads on your PHP version before writing any scraping code:

composer require electrolinux/phpquery

The fork's composer.json declares very old PHP constraints and there is no real CI — expect to hit deprecations or fatal errors on PHP 8.1+. Smoke-test immediately:

use phpQuery;

phpQuery::newDocumentHTML('<div><h2 class="t">Hello</h2></div>');
echo pq('h2.t')->text(); // "Hello"

If that works, the mental model is "jQuery in PHP": phpQuery::newDocumentHTML(), newDocumentFile(), newDocument(), then pq($selector) returns a phpQueryObject you can chain (.find(), .attr(), .text(), .each(), .html()).

Look first at:

  • src/phpQuery.php — static API (newDocument*, unloadDocuments, $defaultCharset).
  • src/phpQuery/phpQueryObject.php — the chainable surface you'll actually use.
  • The pq() helper at the bottom of phpQuery.php — it's global and implicit, which matters for testing.

First realistic use case: a queued job that pulls an external HTML page with Laravel's HTTP client and extracts structured rows into a DTO.

$html = Http::get($url)->body();
phpQuery::newDocumentHTML($html);
$rows = [];
pq('table.results tbody tr')->each(function ($tr) use (&$rows) {
    $rows[] = [
        'name'  => pq($tr)->find('td.name')->text(),
        'price' => pq($tr)->find('td.price')->text(),
    ];
});

Implementation Patterns

Wrap it behind a service. Because pq() is a global function operating on global state, keep all phpQuery calls inside a small dedicated class (e.g. App\Support\HtmlTableParser) that takes a string and returns arrays/DTOs. Controllers, jobs, and Livewire components depend on the service, not on pq().

Fetch outside, parse inside. Use Http::get() / Http::withHeaders([...])->get() for requests (timeouts, retries, proxies, auth) and hand the body to phpQuery. Don't use phpQuery::newDocumentFileURL() in Laravel — you lose HTTP client features and cookies.

Always tear down. For long-lived processes (queues, Octane, Horizon), release documents even when exceptions fire:

try {
    phpQuery::newDocumentHTML($html);
    return pq('.item')->mapInto(...); // or ->each()
} finally {
    phpQuery::unloadDocuments();
}

Loop through fixtures in tests. Store representative HTML in tests/fixtures/*.html, feed strings into the parser service, and assert on the resulting arrays. Never assert on selectors — assert on the DTO shape so you can swap parsers later.

Chunk and cache. For batch scraping, dispatch one job per URL, cache the raw HTML (Cache::put("scrape:{$url}", $html, now()->addMinutes(30))), and cache parsed output separately. This lets you re-parse when selectors change without re-fetching.

Selectors as configuration. Keep CSS selector strings in config or a small array in the parser class rather than scattering them through pq() calls — they will break when the target site redesigns, and centralizing them makes the fix a one-liner.

Chain for ergonomics, extract for clarity. pq('a')->attr('href') returns the first match; pq('a')->texts() returns an array. Use ->each() for row iteration and return plain PHP values from the callback — don't leak phpQueryObject outside the service.

Gotchas and Tips

Abandonware is the primary risk. The package is unmaintained; expect PHP 8 compatibility problems (each() on null, removed string-offset braces, create_function, etc.). Pin the exact version in composer.json and consider vendoring a patched fork, or migrate to symfony/dom-crawler / didanurwuid/didom for new work.

Global document state is the biggest footgun. phpQuery keeps loaded documents in phpQuery::$documents. Loading a second document inside a job silently changes what pq() targets. Call phpQuery::unloadDocuments() between requests, and never call newDocumentHTML() concurrently inside Octane workers.

Encoding. Set phpQuery::$defaultCharset = 'UTF-8' before loading, and pass <?xml encoding="utf-8"?> or a <meta charset> prefix if you get mojibake. mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8') is a common pre-processing workaround.

HTML5 tags get mangled. libxml (which phpQuery wraps) sometimes mangles <section>, <article>, <video> on older Libxml or with bad markup. Wrap unknown tags or use LIBXML_NOERROR | LIBXML_NOWARNING flags where possible.

.text() vs .texts() vs .html(). .text() returns the first match, .texts() returns an array, .html() returns inner HTML of the first match. Selecting multiple elements and calling .text() silently picks one — a frequent source of "why is my list empty."

.dump() prints to stdout. Fine in php artisan tinker, disastrous in HTTP responses or queue logs. Use ->html() / ->text() and pass results to Log::debug() instead.

Selector support is CSS3-ish, not full jQuery. Pseudo-selectors like :contains(), :first, :eq() mostly work; :visible, :animated, and modern :has() generally do not. Prefer plain CSS: pq('tr:nth-child(2)') over pq('tr:eq(1)').

Malformed HTML is common. phpQuery builds an DOMDocument; invalid markup gets repaired unpredictably. Sanitize upstream with tidy_repair_string or just test against real-world samples (not hand-crafted fixtures).

Debugging aids. pq('*') is slow; dump a targeted subtree with pq('.container')->html() first. If a selector fails silently, check whether the document was loaded (.length / ->count()) rather than assuming the selector is wrong.

Extension is not supported cleanly. There is no first-class plugin system despite the jQuery inspiration. Custom helpers belong in your own wrapper class, not via phpQueryObject::extend-style hacks.

Memory leaks. DOMDocument objects and circular references linger. Explicitly unset() the phpQueryObject and call gc_collect_cycles() in batch jobs that parse hundreds of documents.

Consider a shim during migration. If you're already using phpQuery widely, keep the wrapper class and add a parallel implementation using Symfony DomCrawler, switch behind a config flag, and deprecate phpQuery once your tests pass on both.

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.
jinas/dhivatar
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