views:

11680

answers:

12

I have several identical elements with different attributes that I'm accessing with SimpleXML:

<data>
    <seg id="A1"/>
    <seg id="A5"/>
    <seg id="A12"/>
    <seg id="A29"/>
    <seg id="A30"/>
</data>

I need to remove a specific seg element, with an id of "A12", how can I do this? I've tried looping through the seg elements and unsetting the specific one, but this doesn't work, the elements remain.

foreach($doc->seg as $seg)
{
    if($seg['id'] == 'A12')
    {
        unset($seg);
    }
}
+12  A: 

SimpleXML provides no way to remove or reorder XML nodes. Its modification capabilities are somewhat limited. You have to resort to using the DOM extension. dom_import_simplexml() will help you with converting your SimpleXMLElement into a DOMElement.

Just some example code (tested with PHP 5.2.5):

$data='<data>
    <seg id="A1"/>
    <seg id="A5"/>
    <seg id="A12"/>
    <seg id="A29"/>
    <seg id="A30"/>
</data>';
$doc=new SimpleXMLElement($data);
foreach($doc->seg as $seg)
{
    if($seg['id'] == 'A12') {
        $dom=dom_import_simplexml($seg);
        $dom->parentNode->removeChild($dom);
    }
}
echo $doc->asXml();

outputs

<?xml version="1.0"?>
<data><seg id="A1"/><seg id="A5"/><seg id="A29"/><seg id="A30"/></data>

By the way: selecting specific nodes is much more simple when you use XPath (SimpleXMLElement->xpath):

$segs=$doc->xpath('//seq[@id="A12"]');
if (count($segs)>=1) {
    $seg=$segs[0];
}
// same deletion procedure as above
Stefan Gehrig
Thanks for this - initially I was inclined to avoid this answer since I wanted to avoid using DOM. I tried several other answers which did not work, before finally trying yours - which worked flawlessly. To anyone considering avoiding this answer, try it out first and see if it doesn't do exactly what you want. I think what threw me off was I didn't realize dom_import_simplexml() still works with the same underlying structure as the simplexml, so any changes in one immediately effect the other, no need to write/read or reload.
dimo414
Note that this code will only remove the first element encountered. I suspect that this is because modifying the data while it's under iteration invalidates the iterator position, thus causing the foreach loop to terminate. I solved this by saving the dom-imported nodes to an array which I then iterated through to perform the deletion. Not a great solution, but it works.
Ryan Ballantyne
+1  A: 

There is a way to remove a child element via SimpleXml. The code looks for a element, and does nothing. Otherwise it adds the element to a string. It then writes out the string to a file. Also note that the code saves a backup before overwriting the original file.

$username = $_GET['delete_account'];
echo "DELETING: ".$username;
$xml = simplexml_load_file("users.xml");

$str = "<?xml version=\"1.0\"?>
<users>";
foreach($xml->children() as $child){
  if($child->getName() == "user") {
      if($username == $child['name']) {
       continue;
    } else {
       $str = $str.$child->asXML();
    }
  }
}
$str = $str."
</users>";
echo $str;

$xml->asXML("users_backup.xml");
$myFile = "users.xml";
$fh = fopen($myFile, 'w') or die("can't open file");
fwrite($fh, $str);
fclose($fh);
+5  A: 

Just unset the node.

Read the solution here: http://www.kavoir.com/2008/12/how-to-delete-remove-nodes-in-simplexml.html

kavoir.com
A: 

(Originally published by me at youkey.krasnoludken.pl on Thursday, August 20th, 2009 at 11:13 pm )

Idea about helper functions is from one of the comments for DOM on php.net and idea about using unset is from kavoir.com. For me this solution finally worked:

function Myunset($node)
{
 unsetChildren($node);
 $parent = $node->parentNode;
 unset($node);
}

function unsetChildren($node)
{
 while (isset($node->firstChild))
 {
 unsetChildren($node->firstChild);
 unset($node->firstChild);
 }
}

using it: $xml is SimpleXmlElement

Myunset($xml->channel->item[$i]);

The result is stored in $xml, so don’t worry about assigning it to any variable.

Ula Karzelek
A: 

Even though SimpleXML doesn't have a detailed way to remove elements, you can remove elements from SimpleXML by using PHP's unset(). The key to doing this is managing to target the desired element. At least one way to do the targeting is using the order of the elements. First find out the order number of the element you want to remove (for example with a loop), then remove the element:

$target = false;
$i = 0;
foreach ($xml->seg as $s) {
  if ($s['id']=='A12') { $target = $i; break; }
  $i++;
}
if ($target !== false) {
  unset($xml->seg[$target]);
}

You can even remove multiple elements with this, by storing the order number of target items in an array. Just remember to do the removal in a reverse order (array_reverse($targets)), because removing an item naturally reduces the order number of the items that come after it.

Admittedly, it's a bit of a hackaround, but it seems to work fine.

Ilari Kajaste
+1  A: 

For future reference, deleting nodes with SimpleXML can be a pain sometimes, especially if you don't know the exact structure of the document. That's why I have written SimpleDOM, a class that extends SimpleXMLElement to add a few convenience methods.

For instance, deleteNodes() will delete all nodes matching a XPath expression. And if you want to delete all nodes with the attribute "id" equal to "A5", all you have to do is:

// don't forget to include SimpleDOM.php
include 'SimpleDOM.php';

// use simpledom_load_string() instead of simplexml_load_string()
$data = simpledom_load_string(
    '<data>
        <seg id="A1"/>
        <seg id="A5"/>
        <seg id="A12"/>
        <seg id="A29"/>
        <seg id="A30"/>
    </data>'
);

// and there the magic happens
$data->deleteNodes('//seg[@id="A5"]');
Josh Davis
+6  A: 

I believe Stefan's answer is right on. If you want to remove only one node (rather than all matching nodes), here is another example:

//Load XML from file (or it could come from a POST, etc.)
$xml = simplexml_load_file('fileName.xml');

//Use XPath to find target node for removal
$target = $xml->xpath("//seg[@id=$uniqueIdToDelete]");

//If target does not exist (already deleted by someone/thing else), halt
if($target == false)
return; //Returns null

//Import simpleXml reference into Dom & do removal (removal occurs in simpleXML object)
$domRef = dom_import_simplexml($target[0]); //Select position 0 in XPath array
$domRef->parentNode->removeChild($domRef);

//Format XML to save indented tree rather than one line and save
$dom = new DOMDocument('1.0');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadXML($xml->asXML());
$dom->save('fileName.xml');

Note that sections Load XML... (first) and Format XML... (last) could be replaced with different code depending on where your XML data comes from and what you want to do with the output; it is the sections in between that find a node and remove it.

In addition, the if statement is only there to ensure that the target node exists before trying to move it. You could choose different ways to handle or ignore this case.

Witman
Note that xpath() returns an empty array if nothing was found, so the check $target == false should be empty($target). +1 for xpath solution
Znarkus
A: 

Hi Josh Had a look at simpleDom. I have an xml file called articles.xml which contains items with id attributes. How would I go about deleting a node with given id then save the new xml file?

Thank you

Barry
A: 

Your initial approach was right, but you forgot one little thing about foreach. It doesn't work on the original array/object, but creates a copy of each element as it iterates, so you did unset the copy. Use reference like this:

foreach($doc->seg as &$seg) 
{
    if($seg['id'] == 'A12')
    {
        unset($seg);
    }
}
posthy
A: 

a new idea:

simple_xml works as a array.

we can search for the indexes of the "array" we want to delete, and then, use the unset() function to delete this array indexes. my example:

//----------------------------

                    $pos=$this->xml->getXMLUser();
                    $i=0; $array_pos=array();
                    foreach($this->xml->doc->users->usr[$pos]->u_cfg_root->profiles->profile as $profile) {
                        if($profile->p_timestamp=='0') { $array_pos[]=$i; }
                        $i++;
                    }
                    //print_r($array_pos);
                    for($i=0;$i<count($array_pos);$i++) {
                        unset($this->xml->doc->users->usr[$pos]->u_cfg_root->profiles->profile[$array_pos[$i]]);
                    }
joan16v
A: 

This work for me:

    $data = '<data>
    <seg id="A1"/>
    <seg id="A5"/>
    <seg id="A12"/>
    <seg id="A29"/>
    <seg id="A30"/></data>';
$doc = new SimpleXMLElement($data);

$segarr = $doc->seg;

$count = count($segarr);

$j = 0;

for ($i = 0; $i < $count; $i++) {

    if ($segarr[$j]['id'] == 'A12') {
        unset($segarr[$j]);
        $j = $j - 1;
    }
    $j = $j + 1;
}

echo $doc->asXml();
sunnyface45
+2  A: 

If you extend the base SimpleXMLElement class, you can use this method:

class MyXML extends SimpleXMLElement {

    public function find($xpath) {
        $tmp = $this->xpath($xpath);
        return isset($tmp[0])? $tmp[0]: null;
    }

    public function remove() {
        $dom = dom_import_simplexml($this);
        return $dom->parentNode->removeChild($dom);
    }

}

// Example: removing the <bar> element with id = 1
$foo = new MyXML('<foo><bar id="1"/><bar id="2"/></foo>');
$foo->find('//bar[@id="1"]')->remove();
print $foo->asXML(); // <foo><bar id="2"/></foo>
Pies