php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #39593 XPath NodeList: "Couldn't fetch DOMElement: Node no longer exists"
Submitted: 2006-11-22 17:54 UTC Modified: 2006-11-22 20:53 UTC
Votes:12
Avg. Score:4.4 ± 0.6
Reproduced:12 of 12 (100.0%)
Same Version:3 (25.0%)
Same OS:9 (75.0%)
From: dave dot lane at gmx dot net Assigned:
Status: Wont fix Package: DOM XML related
PHP Version: 5.2.0 OS: Linux
Private report: No CVE-ID: None
View Add Comment Developer Edit
Anyone can comment on a bug. Have a simpler test case? Does it work for you on a different platform? Let us know!
Just going to say 'Me too!'? Don't clutter the database with that please — but make sure to vote on the bug!
Your email address:
MUST BE VALID
Solve the problem:
33 + 30 = ?
Subscribe to this entry?

 
 [2006-11-22 17:54 UTC] dave dot lane at gmx dot net
Description:
------------
The following code does not work. When the nodes in a NodeList that is the result of an XPath query are copied into an array and returned from a function. When the nodes in the array are used a warning results: "Couldn't fetch DOMElement. Node no longer exists..."

Reproduce code:
---------------
bug.php:
<?
$DOMMain =& OpenXMLDoc('main.xml');

$aArray =& GetArray($DOMMain);
echo 'After return: '.$aArray[0]->localName->localName."<br>\n";

function &OpenXMLDoc($sFileName) {
        $DOM = new DOMDocument();
        $DOM->preserveWhiteSpace = false;
        $DOM->load($sFileName);
        return $DOM;
}

function &ExecuteXPath($sXPath, DOMDocument &$DOMDocument, DOMNode &$DOMNode) {
        $objXPath = new DOMXPath($DOMDocument);
        return $objXPath->query($sXPath, $DOMNode);
}

function &GetArray(&$DOMMain) {
        $DOMInclude =& OpenXMLDoc('include.xml');

        $NodeImported = $DOMMain->importNode($DOMInclude->documentElement, true);
        $NodeListImported =& ExecuteXPath('./include1', $DOMMain, $NodeImported);
        $NodeListImported = $NodeListImported->item(0)->childNodes;

        echo 'In NodeList: '.$NodeListImported->item(0)->localName."<br>\n";

        $aArray = array();
        foreach($NodeListImported as $Node) {
                $aArray[] = $Node;
        }
        echo 'In Array: '.$aArray[0]->localName."<br>\n";
        return $aArray;
}

?>

main.xml:
<?xml version = "1.0" encoding="ISO-8859-1"?>
<root>
</root>

include.xml:
<?xml version = "1.0" encoding="ISO-8859-1"?>
<include>
        <include1>
                <div>
                        <span>test</span>
                </div>
        </include1>
</include>


Expected result:
----------------
In NodeList: div
In Array: div
After return: div


Actual result:
--------------
In NodeList: div
In Array: div

Warning: Couldn't fetch DOMElement. Node no longer exists in /var/www/html/usr/dla/csc/V2/bug/bug.php on line 5
After return:


Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2006-11-22 17:58 UTC] dave dot lane at gmx dot net
Sorry description should read:

Description:
------------
When the nodes in a NodeList that is the result of an XPath query are copied into an array and returned from a function the Nodes in the array although they exist are not valid. When the nodes in the array are used a warning results: "Couldn't fetch DOMElement. Node no longer exists..."
 [2006-11-22 19:06 UTC] rrichards@php.net
The problem is that you are working with a fragment that's not attached to the document and only have a reference to a node deep within the fragment rather than to the top element of the fragment. When the object holding the fragment ($NodeImported) goes out of scope, the whole fragment is destroyed - as there isn't a way to track fragments without introducing way too much overhead. You still have the DOMElement object, just not the underlying xml data structure.

you either need to return the entire fragment from the function call or remove the nodes you want to return from the  fragment within the array.

i.e. within the foreach() loop add:
$Node->parentNode->removeChild($Node);

This behavior while not optimal is a necessary evil to prevent leaking memory while also keeping system memory usage and performance at an acceptable level when using the DOM extension.
 [2006-11-22 20:53 UTC] dave dot lane at gmx dot net
OK excellent, thanks for the tip. It makes sense and I did suspect it had something to do with scope, I thought it was the NodeList that was causing the problem though.

I think this should be documented though.
 [2010-08-18 11:06 UTC] rodolphe at metaphores dot ch
Hello,

Is there a way to determine if the node is no longer linked to his xml structure without having a warning ?

Thx
 [2017-04-05 08:56 UTC] monier at reinom dot com
Can this be turned into an exception instead of a Warning (still a warning in PHP 7.1.0-x64)?


So far, I rely on a patch that boils up the parentNode of the element, untill either reaching a DOMDocument (meaning node is still there) or a null (meaning node has already been removed). Could this be part of DOMElement's static methods, like a DOMElement::nodeExists(DOMElement $element) ?

<?php
$xml = '<?xml version="1.0" encoding="utf-8"?><block>I have <list><num>0A</num> orange and <list><num>0B</num></list> apple</list>.</block>';
$doc = new DOMDocument();
$doc->loadXml($xml);

$xpath = new DOMXpath($doc);
$nodes = $xpath->query('//num', $doc->documentElement);

foreach ($nodes as $num) {
	if ($num->textContent{0} == '0') {
		// Node might be part of a removed DocumentFragment
		// If so, ignore it: node has already been removed by removing one of its ancestor
		for ($n = $num; !($n instanceof DOMDocument); $n = $n->parentNode) {
			if ($n === null) {
				// Node already removed (see PHP bug #39593) so skip it
				continue 2;
			}
		}
		
		// Will remove the first "list" ancestor of the "num" node
		$a = $xpath->query('ancestor::list[1]', $num);
		if ($a->length === 1) {
			$p = $a->item(0);
			// removed the "list" element
			$p->parentNode->removeChild($p);
		}
	}
}

var_dump($doc->C14N());
?>



Sample: I remove the 1st "list" ancestor element of every "num" element that starts with a "0". Since "list" are nested, the outer one is removed first when "foreaching" the 0A node. Then the OB node is foreached, and its list ancestor is removed. But that ancestor was already removed from document (because it's a descendant of a removed node).
In real production system, I don't know what the XML is, and these nested case may occur (actually, it's not "list" be nested "table" element, and "table" ancestor is removed if a "td" contains a "0"; "table"s can be nested, so removals can be nested at Runtime)
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Fri Mar 29 06:01:29 2024 UTC