Search code examples
phplaravelsimplexmlxdebug

Strange behaviour with xdebug and removing child node in SimpleXMLElement


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.


Solution

  • 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.