getgrav/grav · error · InvalidArgumentException

Invalid arguments, expected DOMElement or DOMDocument

Error message

Invalid arguments, expected DOMElement or DOMDocument

What it means

DOMWordsIterator walks a DOM subtree one word at a time (used by Grav\Common\Helpers\Truncator::truncateWords and the |safe_truncate Twig filters). Like its letters sibling, the constructor accepts only a DOMElement or a DOMDocument (from which it takes documentElement); any other DOMNode — or a document with no root element — fails the instanceof check and throws InvalidArgumentException. The iterator must anchor its recursive word walk on an element.

Source

Thrown at system/src/DOMWordsIterator.php:46

    private $offset = -1;
    /** @var int|null */
    private $key;
    /** @var array<int,array<int,int|string>>|null */
    private $words;

    /**
     * expects DOMElement or DOMDocument (see DOMDocument::load and DOMDocument::loadHTML)
     *
     * @param DOMNode $el
     */
    public function __construct(DOMNode $el)
    {
        if ($el instanceof DOMDocument) {
            $el = $el->documentElement;
        }

        if (!$el instanceof DOMElement) {
            throw new InvalidArgumentException('Invalid arguments, expected DOMElement or DOMDocument');
        }

        $this->start = $el;
    }

    /**
     * Returns position in text as DOMText node and character offset.
     * (it's NOT a byte offset, you must use mb_substr() or similar to use this offset properly).
     * node may be NULL if iterator has finished.
     *
     * @return array
     */
    public function currentWordPosition(): array
    {
        return [$this->current, $this->offset, $this->words];
    }

    /**

View on GitHub (pinned to 6040efed04)

Solutions

  1. Pass a DOMElement: use $doc->documentElement when starting from a DOMDocument, or the wrapper element you created around the fragment
  2. Check for null before constructing: ->item(0) and ->firstChild return null when nothing matched
  3. Load raw HTML through a wrapper first: $doc->loadHTML('<div>' . $html . '</div>') and iterate that div
  4. Guard the call site with an instanceof check to fail with a clearer message

Example fix

// before
$words = new DOMWordsIterator($container); // $container may be null (no <div> found)

// after
$container = $doc->getElementsByTagName('div')->item(0);
if (!$container instanceof DOMElement) {
    return $html; // nothing to truncate
}
$words = new DOMWordsIterator($container->parentNode->removeChild($container));
Defensive patterns

Strategy: type-guard

Validate before calling

$container = $doc->getElementsByTagName('div')->item(0);
if (!$container instanceof DOMElement) {
    return $html; // no wrapper found — skip truncation instead of throwing
}

Type guard

function isTraversableDomRoot($node): bool
{
    if ($node instanceof DOMDocument) {
        $node = $node->documentElement;
    }
    return $node instanceof DOMElement;
}

Prevention

When it happens

Trigger: new DOMWordsIterator($textNode) where the node is a DOMText/DOMAttr/DOMComment; passing the result of an unchecked ->item(0) lookup that returned null; passing an empty DOMDocument (loadHTML of an empty string) whose documentElement is null.

Common situations: Plugins/themes implementing custom safe-truncate Twig filters that walk the DOM and pass the wrong node; processing user-supplied HTML whose structure does not match what the traversal expected; refactors that changed which node gets passed to the iterator.

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/e47927aa37a6d74e. Report an issue: GitHub.