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

Php Simple Html Dom Parser Laravel Package

kub-at/php-simple-html-dom-parser

A fast, lightweight PHP HTML DOM parser that lets you find and extract elements from HTML using jQuery-like selectors. Compatible with simple_html_dom, it makes scraping, parsing, and manipulating markup easy with minimal code.

Deep Wiki
Context7

Getting Started

Install with Composer and start parsing — there is no service provider, no config file, and nothing to publish. That is the entire setup:

composer require kub-at/php-simple-html-dom-parser

Use the namespaced facade rather than the global str_get_html() / file_get_html() functions, so you stay autoload-safe and greppable:

use KubAT\PhpSimple\HtmlDomParser;

$dom = HtmlDomParser::str_get_html('<div class="price">$19.99</div>');

$price = $dom->find('div.price', 0)->plaintext; // "$19.99"
$dom->clear();

The first thing to read in the vendor directory is the bundled upstream manual shipped inside the package source (src/simplehtmldom/manual/) — it is the only real documentation for the selector syntax and the simple_html_dom_node API, and it is more current than most blog posts about this library. Skim the "quick start" and "find" sections and you have 90% of what you need.

Your first realistic use case in a Laravel app is not scraping a whole site — it is extracting a few fields from an HTML blob you already have (an emailed invoice, a webhook payload, a legacy HTML report, a scraped product page). Wire it into a throwaway artisan command or a test first:

$dom = HtmlDomParser::str_get_html($html);
$title = $dom->find('h1', 0)?->plaintext;

Because the package returns a mutable, memory-heavy simple_html_dom tree rather than a query builder, decide early where in your codebase the "parse and immediately map to plain PHP arrays/DTOs" boundary lives — usually a single app/Support/Html/ service class. Everything downstream should never see a simple_html_dom object.

Implementation Patterns

Always fetch with Laravel's HTTP client, then parse. HtmlDomParser::file_get_html() exists, but it is file_get_contents() in a trench coat: no timeouts you control, no status-code handling, no retries, and it fails outright if allow_url_fopen is off. Treat the library as a pure parser:

use Illuminate\Support\Facades\Http;
use KubAT\PhpSimple\HtmlDomParser;

$response = Http::withHeaders(['User-Agent' => 'Mozilla/5.0 (compatible; MyApp/1.0)'])
    ->timeout(15)
    ->retry(3, 200)
    ->get($url)
    ->throw();

$dom = HtmlDomParser::str_get_html($response->body());

Wrap it in a small injectable service so you can mock it, swap it later for symfony/dom-crawler, and keep clear() calls in one place:

namespace App\Support\Html;

use KubAT\PhpSimple\HtmlDomParser;

class HtmlParser
{
    public function parse(string $html): \simple_html_dom
    {
        return HtmlDomParser::str_get_html(
            $html,
            lowercase: true,
            forceTagsClosed: false, // don't mangle HTML5 void elements
            stripRN: false,         // preserve newlines inside <pre>
        );
    }

    /**
     * @return array<int, array<string, string|null>>
     */
    public function rows(string $html, string $rowSelector, array $fields): array
    {
        $dom = $this->parse($html);

        try {
            return collect($dom->find($rowSelector))
                ->map(fn ($row) => collect($fields)
                    ->mapWithKeys(fn ($selector, $key) => [
                        $key => $this->text($row->find($selector, 0)),
                    ])
                    ->all())
                ->all();
        } finally {
            $dom->clear();
            unset($dom);
        }
    }

    private function text(?\simple_html_dom_node $node): ?string
    {
        return $node ? trim(preg_replace('/\s+/u', ' ', $node->plaintext)) : null;
    }
}

Extraction cheatsheet. find() takes a CSS-subset selector and an optional index (0 = first match, returns null if absent). It returns a plain array, so collect() composes naturally:

$dom->find('a[href^="/products/"]');       // attribute prefix
$dom->find('img[data-src]');               // attribute presence
$dom->find('div.card > h2.title');         // child combinator
$dom->find('table#prices tbody tr');       // id + descendant
$node->attr['href'] ?? null;               // attribute access (preferred)
$node->plaintext;                          // visible text
$node->innertext;                          // inner HTML
$node->outertext;                          // node's own HTML
$node->children();                         // array of nodes
$node->parent(); $node->next_sibling();    // tree navigation

Map to DTOs/collections at the boundary rather than passing nodes around. A queued job that returns collect($dom->find('.product'))->map(fn ($n) => ProductData::fromNode($n)) is testable, serialisable, and immune to the tree being cleared.

DOM rewriting is a genuine use case. Because innertext and outertext are writable, you can sanitise or rewrite stored HTML — strip <script>, rewrite relative links to absolute, remove tracking pixels:

$dom = HtmlDomParser::str_get_html($storedHtml);
foreach ($dom->find('script, iframe') as $node) {
    $node->outertext = '';
}
foreach ($dom->find('a[href]') as $a) {
    if (str_starts_with($a->href, '/')) {
        $a->href = url($a->href);
    }
}
$clean = $dom->save();
$dom->clear();

For anything queued or bulk, use Bus::batch() with a per-host RateLimiter, cache the parsed arrays (Cache::remember("scrape:{$url}", now()->addHours(6), ...)) and never hold multiple simple_html_dom objects alive at once — see the memory note below.

Testing is straightforward because parsing is pure: Http::fake() the response, point it at a fixture in tests/Fixtures/*.html, and assert on the mapped arrays. No network, no flakiness, no mocking of the parser itself.

Gotchas and Tips

  • Memory leaks are real and cumulative. The upstream library is notorious for this; each parsed document holds a full node tree with circular parent/child references. In long-running workers (queues, Octane, artisan loops) you must call $dom->clear() in a finally block and unset() the variable. Without it a Horizon worker will balloon over a few hundred documents and get killed. Consider a dedicated queue with --max-jobs set low.
  • stripRN defaults to true and silently removes \r/\n. That is fine for prose but destroys <pre>, <textarea>, and code blocks. Pass stripRN: false when the source is code or whitespace-sensitive.
  • forceTagsClosed defaults to true and will rewrite HTML5 void elements (<br>, <img>, <meta>) in ways that occasionally break round-tripping. Set it to false for HTML5 input; leave it true for tag-soup legacy HTML where you actually want it repaired.
  • The selector engine is a subset, not CSS. Tag, #id, .class, attribute operators (=, !=, *=, ^=, $=) and the space/> combinators work. Pseudo-classes (:nth-child, :not, :first-of-type) and grouped selectors (div, span) generally do not — run two find() calls and merge instead. There is no ->text() helper; use ->plaintext.
  • file_get_html() is a trap in production. It ignores HTTP status codes (you get the 404 page and parse it happily), has no meaningful timeout, sends a default user-agent that many sites block, and dies if allow_url_fopen=0. Also never pass user-supplied URLs to it — that is a textbook SSRF. Use Http::get() and str_get_html().
  • $node->foo is ambiguous. Magic __get returns the attribute foo if present, otherwise a child element named <foo>. $node->title on a <div title="x"><title>y</title></div> will surprise you. Prefer $node->attr['title'] for attributes and $node->find('title', 0) for children.
  • Charset handling is on you. Input must be UTF-8. If the source is ISO-8859-1 or Shift-JIS, detect from the meta tag or response header and run mb_convert_encoding($html, 'UTF-8', $charset) before parsing; the $target_charset parameter will not rescue a mis-decoded source. Also run html_entity_decode(..., ENT_QUOTES, 'UTF-8') on plaintext if you need &nbsp;/&mdash; resolved — the built-in entity table is incomplete.
  • PHP 8.x deprecations. The last release is from October 2019, so expect notices on PHP 8.1+ (null-to-non-nullable arguments, implicit nullable params) in strict error handlers. If deprecations are fatal in your environment, either pin an error handler around the parse or switch to the actively maintained voku/simple_html_dom fork, which shares the same API shape and adds better CSS selector support plus PHP 8 fixes.
  • No Laravel integration means no extension points. There is no config, no custom selector engine, and the node classes are hard to subclass meaningfully. Treat it as a leaf dependency: keep it behind your own HtmlParser interface so migrating to symfony/dom-crawler + symfony/css-selector (the better long-term choice for anything resembling a crawler) is a one-file change.
  • Check the maintenance budget. 152 stars, MIT, last release 2019 — it works, it is stable, and the HTML spec has not changed much, but do not file issues expecting fixes. Vendor-patch anything critical and pin the version in composer.json.
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.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment