{"record":{"id":"d04ffbfad8e3ba6a","repo":"getgrav/grav","slug":"invalid-arguments-expected-domelement-or-domdocum","errorCode":null,"errorMessage":"Invalid arguments, expected DOMElement or DOMDocument","messagePattern":"Invalid arguments, expected DOMElement or DOMDocument","errorType":"exception","errorClass":"InvalidArgumentException","httpStatus":null,"severity":"error","filePath":"system/src/DOMLettersIterator.php","lineNumber":47,"sourceCode":"    private $offset = -1;\n    /** @var int|null */\n    private $key;\n    /** @var array<int,string>|null */\n    private $letters;\n\n    /**\n     * expects DOMElement or DOMDocument (see DOMDocument::load and DOMDocument::loadHTML)\n     *\n     * @param DOMNode $el\n     */\n    public function __construct(DOMNode $el)\n    {\n        if ($el instanceof DOMDocument) {\n            $el = $el->documentElement;\n        }\n\n        if (!$el instanceof DOMElement) {\n            throw new InvalidArgumentException('Invalid arguments, expected DOMElement or DOMDocument');\n        }\n\n        $this->start = $el;\n    }\n\n    /**\n     * Returns position in text as DOMText node and character offset.\n     * (it's NOT a byte offset, you must use mb_substr() or similar to use this offset properly).\n     * node may be NULL if iterator has finished.\n     *\n     * @return array\n     */\n    public function currentTextPosition(): array\n    {\n        return [$this->current, $this->offset];\n    }\n\n    /**","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/DOMLettersIterator.php#L29-L65","documentation":"DOMLettersIterator walks an HTML/DOM subtree one character at a time (used by Grav\\Common\\Helpers\\Truncator::truncateLetters). Its constructor only accepts a DOMElement, or a DOMDocument from which it takes documentElement. If you hand it any other DOMNode subtype — DOMText, DOMAttr, DOMComment — or a DOMDocument that has no documentElement (empty/failed load), the instanceof check fails and it throws InvalidArgumentException. The iterator needs a real element as its recursion root, so it refuses anything else.","triggerScenarios":"Calling new DOMLettersIterator($node) where $node is a DOMText/DOMAttr/DOMComment (e.g. a node grabbed via ->firstChild or ->nodeValue's parent), passing null (e.g. getElementsByTagName('div')->item(0) returned null because the markup had no div), or passing a DOMDocument produced by loadHTML('') whose documentElement is null.","commonSituations":"Custom Twig filters or plugins that truncate HTML and pass the wrong node from a DOM walk; feeding Truncator-style code malformed HTML so the expected wrapper element is missing; code upgraded from a version that silently tolerated other node types.","solutions":["Pass the element itself: for a DOMDocument use $doc->documentElement, for a fragment use the wrapper DOMElement (Truncator uses the <div> it injected via loadHTML(\"<div>$html</div>\"))","Null-check any node fetched by ->item(0)/->firstChild before constructing the iterator","If starting from raw HTML, wrap and load it first: $doc = new DOMDocument(); $doc->loadHTML('<div>' . $html . '</div>'); then pass $doc or its documentElement","Add an instanceof DOMElement/DOMDocument guard at your call site so the failure surfaces with your own context"],"exampleFix":"// before\n$iterator = new DOMLettersIterator($node); // $node may be DOMText or null\n\n// after\nif ($node instanceof DOMDocument) {\n    $node = $node->documentElement;\n}\nif (!$node instanceof DOMElement) {\n    throw new InvalidArgumentException('Expected DOMElement, got ' . get_class($node));\n}\n$iterator = new DOMLettersIterator($node);","handlingStrategy":"type-guard","validationCode":"if ($node instanceof DOMDocument) {\n    $node = $node->documentElement;\n}\nif (!$node instanceof DOMElement) {\n    throw new InvalidArgumentException('DOMLettersIterator needs a DOMElement, got ' . (is_object($node) ? get_class($node) : gettype($node)));\n}","typeGuard":"/**\n * True when DOMLettersIterator/DOMWordsIterator will accept the node.\n * @param DOMNode|DOMDocument|null $node\n */\nfunction isTraversableDomRoot($node): bool\n{\n    if ($node instanceof DOMDocument) {\n        $node = $node->documentElement;\n    }\n    return $node instanceof DOMElement;\n}","tryCatchPattern":null,"preventionTips":["Never pass ->firstChild/->item(0) results unchecked — they are null when nothing matched","When loading raw HTML, always wrap it: $doc->loadHTML('<div>' . $html . '</div>') so an element root is guaranteed","Remember an empty DOMDocument has a null documentElement; guard that case explicitly"],"tags":["php","dom","html-parsing","invalid-argument","iterator"],"backgroundTag":"invalid-constructor-argument","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}