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

Zend Dom Laravel Package

zendframework/zend-dom

Zend\Dom provides tools for parsing and querying HTML and XML documents, including CSS selector support translated to XPath via Zend\Dom\Query. A legacy Zend Framework component for DOM inspection and extraction.

Deep Wiki
Context7

Getting Started

zendframework/zend-dom is a small, framework-agnostic DOM querying component. In Laravel you will mostly use it as a convenience layer over DOMDocument/DOMXPath so you can query HTML with CSS selectors instead of hand-written XPath.

Important first step: the package is archived and abandoned. On a modern Laravel app (PHP 8+) Composer may refuse it or you may hit runtime incompatibilities. Prefer laminas/laminas-dom (same API, maintained namespace Laminas\Dom) or symfony/dom-crawler. If you are maintaining a legacy PHP 7 app and must use the Zend package:

composer require zendframework/zend-dom

Look first at:

  • vendor/zendframework/zend-dom/src/Query.php — the main entry point.
  • vendor/zendframework/zend-dom/src/NodeList.php — what execute() returns.
  • vendor/zendframework/zend-dom/src/Document.php and src/Document/Query.php — lower-level document/query helpers if you need them.

Your first use case should be parsing a known HTML fragment and extracting nodes:

use Zend\Dom\Query;

$html = <<<'HTML'
<html><body>
  <div class="product" data-id="42">
    <h2>Widget</h2>
    <a href="/products/42">Details</a>
  </div>
</body></html>
HTML;

$dom = new Query($html);

foreach ($dom->execute('div.product') as $node) {
    $link = $node->getElementsByTagName('a')->item(0);

    dump([
        'id'    => $node->getAttribute('data-id'),
        'title' => trim($node->getElementsByTagName('h2')->item(0)->textContent),
        'url'   => $link?->getAttribute('href'),
    ]);
}

That is the core workflow: construct Query with HTML, call execute() with a CSS selector (or XPath), iterate the resulting NodeList of DOMNode objects.

Implementation Patterns

Wrap parsing in a dedicated service

Do not sprinkle new Query(...) through controllers. Keep the dependency at the edge of your app so you can replace it later with Laminas or Symfony DomCrawler.

namespace App\Services;

use Zend\Dom\Query;

final class ProductPageParser
{
    /**
     * @return array<int, array{id: string, title: string, url: ?string}>
     */
    public function parse(string $html): array
    {
        $dom = new Query($html, 'UTF-8');
        $products = [];

        foreach ($dom->execute('div.product') as $node) {
            $titleNode = $node->getElementsByTagName('h2')->item(0);
            $linkNode  = $node->getElementsByTagName('a')->item(0);

            if (! $titleNode) {
                continue;
            }

            $products[] = [
                'id'    => $node->getAttribute('data-id'),
                'title' => trim($titleNode->textContent),
                'url'   => $linkNode?->getAttribute('href'),
            ];
        }

        return $products;
    }
}

Use it inside Artisan commands and queued jobs

Typical Laravel day-to-day usage is scraping a vendor page or parsing a third-party HTML response:

use Illuminate\Support\Facades\Http;
use App\Services\ProductPageParser;

$response = Http::timeout(10)->get($url)->throw();
$products = app(ProductPageParser::class)->parse($response->body());

If the source is slow or unreliable, put the fetch + parse in a queued job. Keep the parser synchronous and testable; let the job handle retries, timeouts, and persistence.

Prefer CSS selectors for readability, XPath for power

CSS selectors are the reason to use this package over raw DOMDocument:

$dom->execute('table.results tbody tr');
$dom->execute('a[rel="next"]');
$dom->execute('div.alert.error');

When CSS cannot express the relationship, use XPath directly. Query::execute() accepts XPath as well, typically detected by leading // or /:

$dom->execute('//div[contains(@class, "product")]/following-sibling::div[1]');

Keep a rule of thumb: CSS for structure and classes, XPath for text matching, sibling traversal, parent selection, or predicates.

Convert NodeList into plain arrays immediately

NodeList contains live DOM nodes. Do not pass it across layers or store it in a job payload. Normalize to arrays or DTOs as soon as possible:

$items = [];

foreach ($dom->execute('.item') as $node) {
    $items[] = [
        'text' => trim($node->textContent),
        'href' => $node->getAttribute('href'),
    ];
}

Resolve relative URLs explicitly

getAttribute('href') returns exactly what is in the HTML. Resolve it with Laravel’s URL helpers or GuzzleHttp\Psr7\UriResolver:

use Illuminate\Support\Uri;

$href = $linkNode?->getAttribute('href');

$absolute = $href
    ? (string) Uri::of($baseUrl)->withPath($href) // or use url($href) for simple cases
    : null;

Be careful with url() for protocol-relative and absolute URLs; use a proper URI resolver when scraping.

Test parsers with fixture HTML

Keep fixture files in tests/Fixtures/*.html and assert the normalized output, not DOM internals:

$parser = new ProductPageParser();
$products = $parser->parse(file_get_contents(base_path('tests/Fixtures/products.html')));

$this->assertCount(3, $products);
$this->assertSame('Widget', $products[0]['title']);

This is also the best way to verify which CSS selectors the component actually supports before relying on them in production.

Integration tip: isolate encoding normalization

DOMDocument is sensitive to encoding. Normalize before handing HTML to Query:

private function normalizeHtml(string $html): string
{
    if (! mb_check_encoding($html, 'UTF-8')) {
        $html = mb_convert_encoding($html, 'UTF-8', 'ISO-8859-1');
    }

    // Helps DOMDocument interpret UTF-8 correctly when the source omits a charset meta tag.
    return '<?xml encoding="UTF-8">' . $html;
}

Then construct new Query($this->normalizeHtml($html), 'UTF-8').

Gotchas and Tips

The biggest gotcha: the package is archived

zendframework/zend-dom is abandoned and Composer will usually mark it as such. On Laravel 9/10/11 with PHP 8+, installation or runtime may fail because of old PHP constraints. For new work:

  • Use laminas/laminas-dom if you want the same Query/Document API under the Laminas\Dom namespace.
  • Use symfony/dom-crawler if you want a more actively maintained CSS-selector crawler.
  • Use native DOMDocument + DOMXPath if you only need a handful of queries.

If you must stay on Zend, pin PHP 7.x or maintain a fork, and document the migration path in the service wrapper.

CSS selector support is not full browser CSS

The CSS-to-XPath conversion is basic. Do not assume every selector you use in CSS or jQuery works. Pseudo-selectors, some attribute operators, and complex combinators may be unsupported or behave differently. Test each selector against a fixture. When in doubt, fall back to XPath:

$dom->execute('//a[starts-with(@href, "/products/")]');

Malformed HTML triggers libxml warnings

Real-world HTML is often broken. Wrap parsing with libxml error handling and restore global state:

$previous = libxml_use_internal_errors(true);

try {
    $dom = new Query($html);
    $nodes = $dom->execute('.item');
} finally {
    libxml_clear_errors();
    libxml_use_internal_errors($previous);
}

Do not let libxml warnings become Laravel exception noise unless you explicitly convert them.

DOMDocument will wrap fragments

If you pass a fragment such as <div>Hello</div>, DOMDocument will usually add html/body wrappers. Selectors still work, but saveHTML() output will not be byte-identical to the input. If fragment fidelity matters, use native DOMDocument::loadHTML with appropriate libxml flags or a different package.

textContent vs nodeValue

For element nodes, use textContent and trim() it. nodeValue can behave differently for attributes and text nodes. For links, read getAttribute('href'), not nodeValue.

NodeList is live

If you remove or replace nodes while iterating, you can skip elements. Convert to an array first when mutating:

$nodes = iterator_to_array($dom->execute('.item'));

Encoding produces mojibake surprisingly often

Symptoms: é instead of é, or empty text for non-ASCII content. Causes:

  • Missing <meta charset="utf-8"> in the source.
  • Passing UTF-8 bytes to DOMDocument without declaring the encoding.
  • Double-encoding after mb_convert_encoding.

Prefer passing the encoding to Query and adding an XML encoding declaration or meta charset before parsing. Test with accented characters and emoji.

Relative URLs and base tags

The package does not resolve URLs for you. If the page has a <base href="...">, read it and resolve against that rather than the request URL.

XML namespaces need XPath care

If you parse XML with namespaces, CSS selectors may not match as expected. Use XPath with registered prefixes via DOMXPath for namespaced documents, or strip/normalize namespaces before querying.

No Laravel integration points

There is no service provider, facade, config file, or Blade directive. Treat it as a plain library. The only “config” is the encoding you pass and the HTML you feed it. Extension points are therefore:

  • Wrap it in your own parser/service class.
  • Subclass Zend\Dom\Query if you need custom selector normalization.
  • Use the lower-level Zend\Dom\Document/Document\Query classes if you need more control than Query::execute().
  • Swap the implementation behind your wrapper to Laminas or Symfony without changing callers.

Do not use it for sanitization

This is a querying component, not an HTML sanitizer. Parsing untrusted HTML for display or storage requires a dedicated sanitizer such as ezyang/htmlpurifier or Symfony’s HTML sanitizer. zend-dom will happily preserve dangerous markup in the DOM.

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.
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
uni-method/symfony-lib