martyrer/simple-html-dom-bundle
A Symfony bundle that integrates PHP Simple HTML DOM into your application, giving you a convenient service for parsing and manipulating HTML using jQuery-like selectors. Load pages, traverse the DOM, and extract or edit elements with straightforward, readable syntax.
Know what you're installing before you install anything. This package is a Symfony bundle, not a Laravel package. It ships a Bundle subclass plus DI service definitions for simple_html_dom (S.C. Chen / sunra, global functions, no namespace, no PSR-4). Laravel has no PackageManifest entry, no service provider, no facade, no config to publish — so composer require will either fail on symfony/framework-bundle constraints or install dead weight you can't autoload into the container. From a Laravel codebase, treat the evaluation target as the underlying library, not the bundle.
If you still want to run it (e.g., in a Symfony side-service or a scratch container to compare against your current stack), register the bundle the way Symfony expects:
// config/bundles.php (Symfony 5+)
return [
Martyrer\SimpleHtmlDomBundle\SimpleHtmlDomBundle::class => ['all' => true],
];
Then confirm what it actually registered by reading, in order:
vendor/martyrer/simple-html-dom-bundle/DependencyInjection/*Extension.php — the real service ID (if any).Resources/config/services.* — the alias name you should type-hint.simple_html_dom.php header — the upstream version it vendored in.Many bundles of this shape do nothing but require the library file so the global functions exist. Verify before assuming a container service exists.
The canonical "does it work" check:
$html = str_get_html('<div class="price">9.99</div>');
echo $html->find('div.price', 0)->plaintext; // "9.99"
$html->clear();
If this prints 9.99, the wrapper did its one job. Everything past this line is you learning simple_html_dom, not the bundle.
Laravel-native starting point. If your real question is "how do we scrape/parse HTML in a Laravel app," skip the bundle entirely and start from one of these:
voku/simple_html_dom — a maintained, namespaced, Composer-installable fork of the same library, with the same selector API. Drop-in for existing call sites.symfony/dom-crawler + symfony/css-selector — already a common transitive dep, actively maintained, faster, better error semantics.Dom\HTMLDocument — no dependency at all for straightforward extraction.Bind the library yourself, one class, one binding. If you do adopt simple_html_dom (for its tolerance of badly formed markup), wrap it in a small app-owned class and register it in a service provider. That is roughly thirty minutes of work and gives you a seam to swap the parser later.
// app/Services/HtmlExtractor.php
namespace App\Services;
class HtmlExtractor
{
public function fromHtml(string $html): \simple_html_dom
{
$dom = str_get_html($html);
if ($dom === false) {
throw new \RuntimeException('Unparseable HTML document');
}
return $dom;
}
public function fromUrl(string $url): \simple_html_dom
{
$response = \Illuminate\Support\Facades\Http::timeout(10)
->withHeaders(['User-Agent' => 'Mozilla/5.0 (compatible; App/1.0)'])
->retry(2, 200)
->get($url);
$response->throw();
return $this->fromHtml($response->body());
}
}
// app/Providers/AppServiceProvider.php → register()
$this->app->singleton(HtmlExtractor::class);
Never call file_get_html(). It wraps file_get_contents — no timeout, no user agent, no redirect policy, no TLS verification control, no error handling worth the name, and it puts SSRF/local-file reach squarely in your request path. Fetch with Http:: or Guzzle, hand the body to str_get_html. The split also lets you swap the HTTP layer (proxies, rate-limiters, caches) independently of the parser.
Use the selector API the way it's meant to be used; don't reimplement CSS.
$dom = $extractor->fromHtml($body);
// Grab first match, guard for null.
$title = optional($dom->find('h1.title', 0))->plaintext;
// Iterate collections.
foreach ($dom->find('table#results tr') as $row) {
$cells = array_map(fn($c) => trim($c->plaintext), $row->find('td'));
// ...
}
$dom->clear(); // critical — see Gotchas
Rely on find() for selection, plaintext for text, ->href / ->src / ->attr for attributes. Do not build your own regex pass on top; you will lose to encoding and nesting edge cases the library already handles (badly, but better than regex).
Isolate the call site from the parser type. Return arrays/plain PHP structures out of your extractor, not simple_html_dom objects. That single discipline is what makes the eventual migration to DomCrawler or native Dom\HTMLDocument a one-file change per feature instead of a rewrite across twenty services.
public function prices(string $html): array
{
$dom = $this->fromHtml($html);
try {
return collect($dom->find('div.price'))
->map(fn($n) => (float) trim($n->plaintext))
->all();
} finally {
$dom->clear();
}
}
Integration tips for job-driven workflows:
ShouldQueue job, one document per job, with $timeout and $tries set explicitly.str_get_html (strlen($body) > 2_000_000 → reject) — this is the single cheapest protection against worker OOM.RateLimiter::for('scraper-'.$host, ...) — an unbounded foreach over URLs is a self-inflicted outage.Symfony bundle in a Laravel codebase. The class extends Symfony\Component\HttpKernel\Bundle\Bundle. Laravel cannot auto-discover it and there is no ServiceProvider to register. If you --ignore-platform-reqs your way past the Composer conflict, you still have symfony/framework-bundle constraints fighting against Laravel's symfony/* v6/v7. Do not work around Composer to make a wrapper fit. If you need the library, composer require voku/simple_html_dom or vendor the file.
Memory is the real ceiling, not CPU. simple_html_dom builds a full object tree, commonly 10–100× the source document. The failure mode is a queue worker silently hitting memory_limit and dying mid-job, producing no error row — just a stuck job. Mitigations, in order:
$dom->clear() in a finally block, every time.max_execution_time and job timeouts set below the worker's memory-kill threshold.null and false are returned inconsistently. str_get_html() returns false on failure; find($selector, 0) returns null when nothing matches. A find(...)->plaintext chain therefore throws a null-property error far away from the parse site, in code that looks unrelated. Always guard: optional($dom->find('h1', 0))->plaintext ?? '', or wrap the fetch/parse in a typed extractor that throws on ambiguity.
PHP 8 compatibility is not guaranteed. The legacy simple_html_dom codebase was written pre-PHP 7 and regularly trips on PHP 8 strict typing, deprecations, and #[\ReturnTypeWillChange] failures. Before assuming it "just works" on PHP 8.1–8.4:
each(, create_function, and {}-style array access — all removed in PHP 8.Global function and class collisions. The library defines str_get_html, file_get_html, and the global simple_html_dom class in the root namespace. Anything else in the autoload set that defines the same names — another scraper, a legacy helper, a polyfill — will fatal on include with no useful message. If you must coexist, wrap all access behind your own service and consider autoload.files discipline.
Encoding will corrupt data silently. simple_html_dom predates robust UTF-8 handling. On non-ASCII content, mojibake frequently passes through and only surfaces downstream (search, matching, exports). Force charset explicitly:
$dom = str_get_html($body, true, true, DEFAULT_TARGET_CHARSET, false);
$dom->load($body) && null;
// Or normalize first with mb_convert_encoding($body, 'UTF-8', 'auto')
Then assert in tests: parse a document with curly quotes / accented characters and verify round-trip. Without that fixture you will not catch the corruption until a customer does.
Selectors are not full CSS. simple_html_dom supports a practical subset (tag, class, id, attribute, descendant, comma-OR, [attr=value]). It does not support full CSS4 pseudo-classes (:has(), :nth-child(of ...), :is()), and behavior on :nth-child and attribute operators differs from the spec. Anything you write that works here should be considered simple_html_dom dialect, not CSS — and re-tested if you ever migrate to DomCrawler or native DOM.
Debugging: log the raw body, not the DOM. When a parse fails or a selector returns nothing, the DOM object is unhelpful — no pretty-printed tree, no error line. Always stash the raw HTML (S3, log, a storage/app/scrape/ fixture folder) before parsing so you can reproduce. This also gives you characterization fixtures for an eventual migration off the library.
Extension points are minimal by design. The bundle is a shim; there is essentially nothing to extend. The real extension point is your own extractor class — that's where you add retries, size caps, caching, and selector helpers. Do not try to monkey-patch the bundle; it will be overwritten on the next composer update, if the maintainer ever ships one (they likely won't — zero stars, zero dependents, no release cadence to speak of).
On the "reject vs. adopt" call. This package is a convenience wrapper around a decaying parser with no ecosystem validation. If HTML extraction is a real product need, put the ~1–2 engineer-days into DomCrawler or native Dom\HTMLDocument behind an owned interface. If it's a one-off migration script, script it with voku/simple_html_dom and throw it away when the job finishes. In both cases, the bundle itself is not in the answer.
How can I help you explore Laravel packages today?