I have a xml document with multidimensional structure:
<?xml version="1.0" encoding="UTF-8"?>
<main>
<products>
<product id="87" state="active">
<options>
<option id="99" state="active">
<item id="33" value="somevalue" />
<item id="35" value="somevalue2" />
</option>
<option id="12" state="deleted">
<item id="56" value="somevalue" />
<item id="34" value="somevalue2" />
</option>
</options>
<reports>
<report type="json">
<field id="123" state="active" />
<field id="234" state="deleted" />
<field id="238" state="active" />
<field id="568" state="deleted" />
</report>
</reports>
</product>
</products>
</main>
In the PHP backend I've written methods to detect items with "deleted" status and remove them. Here is PHP part:
public function loadAndModify() {
$xml = simplexml_load_file($this->request->file('import_xml'));
$this->processXml($xml);
}
/**
* @param $element
*
* @return bool
*/
private function shouldRemove($element): bool
{
return ($element['state'] == SomeClass::STATE_DELETED);
}
/**
* @param $xml
*
* @return void
*/
private function processXml(&$xml): void
{
if ($xml->children()->count() > 0) {
foreach ($xml->children() as $child) {
if ($this->shouldRemove($child)) {
// this code works as expected with or without xdebug
//$node = dom_import_simplexml($child);
//$node->parentNode->removeChild($node);
// this code will work only with xdebug when breakpoint is set
unset($child[0]);
continue;
// end
} else {
$this->processXml($child);
}
}
}
}
I solve my problem by converting simpleXMLElement to DOMElement.
However it seems that PHP has some bug when I use unset
with xdebug. When I add breakpoint to line with unset
and go to next step in the debugger and then resume application - there is no problem. But when breakpoint is active and I just clicked resume application it cause error:
Uncaught ErrorException: Trying to get property of non-object in \project\vendor\symfony\var-dumper\Cloner\AbstractCloner.php
If someone else had this error please explain why this is happened in this case. Thanks.
As discussed in the comments under this previous answer, the problem you are encountering is that you are manipulating an object (in this case, the return value of $xml->children()
) which you are iterating over (with a foreach
loop).
Internally, the SimpleXMLElement
object has a list of child items it is going to present, in turn, to the iterator code in foreach
. When you delete the current child item, you necessarily change the shape of that internal list, so "next item" is not well defined. Deleting other items in the list can also have odd behaviour - for instance, deleting item 1 while inspecting item 2 may cause the iterator to "skip ahead" since item 4 has now moved into the place where item 3 was.
As hakre suggests in the comments linked above, the most robust solution is to copy the original list of items into an array, which can be achieved using iterator_to_array
. Passing false
as the second argument throws away the keys, which is important with SimpleXML because because it uses the tag name as the key, and there can be only one value for each key in the array.
foreach ( iterator_to_array($xml->children(), false) as $child) {
// Carry on as you were
}
The only thing to be aware of with this is that iterator_to_array
will go through the whole list before returning, so if you have a large list and want to break out of the loop early, or stream output, this may be problematic.