use setAttribute and createElement | PHP XML - php

This code:
$product = $dom->createElement('o');
$product->setAttribute('id', $productSku);
$product->setAttribute('url', $productWebsite.$productUrl);
$product->setAttribute('price', substr_replace($productPrice,'.',-2,0).$productCurrency);
$product->setAttribute('avail', $productAvail);
$product->setAttribute('weight', $productWeight);
$product->setAttribute('stock', $productStock);
$product->setAttribute('basket', $productBasket);
Generate for me:
<offers><o id="12264" url="http" price="209.22" avail="1" weight="1" stock="183" basket="1">
Now I add:
//tag
$title = $dom->createElement('name', clean($productTitle));
$product->appendChild($title);
and under this xml line I have
<name>some name</name>
everything is okay! But I need add next tags under this all lines to get result:
<imgs>
<main url="https://firstimage1"/>
</imgs>
so now I try:
$product = $dom->createElement('imgs');
$product->setAttribute('main', $productImagePath.$productImage);
What is issue?
This code replace first tag o with imgs and tag main add to first tag with <o some like this by end of line:
<offers><imgs id="12264" url="http" price="209.22" avail="1" weight="1" stock="183" basket="1" main="https:urlphoto">
This is wrong. I need to get full effect:
<offers><o id="12264" url="http" price="209.22" avail="1" weight="1" stock="183" basket="1">
<name>some name</name>
<imgs>
<main url="https://firstimage1"/>
</imgs>
</o>
</offers>
full code:
$product = $dom->createElement('o');
$product->setAttribute('id', $productSku);
$product->setAttribute('url', $productWebsite.$productUrl);
$product->setAttribute('price', substr_replace($productPrice,'.',-2,0).$productCurrency);
$product->setAttribute('avail', $productAvail);
$product->setAttribute('weight', $productWeight);
$product->setAttribute('stock', $productStock);
$product->setAttribute('basket', $productBasket);
//tag
$title = $dom->createElement('name', clean($productTitle));
$product->appendChild($title);
// next element (issue here) this replace first tags
$product = $dom->createElement('imgs');
$product->setAttribute('main', $productImagePath.$productImage);
$root->appendChild($product);
}
$dom->appendChild($root);
$dom->save($filePath);
}
#update:
I try change:
// next element
// next element
$product2 = $dom->createElement('imgs');
$product3 = $dom->createElement('main');
$product3->setAttribute('url', $productImagePath.$productImage);
and by end of file:
$root->appendChild($product);
$root->appendChild($product2);
$root->appendChild($product3);
But now I have:
<offers>
<o id="12264" url="http" price="209.22" avail="1" weight="1" stock="183" basket="1">
<name>4711 Eau De Cologne 800ml</name>
</o>
<imgs/>
#update 2:
#Thank you for answear. This working correct! Can I ask you about last thing.
I need add again:
<attrs>
<a name="Producent">
<![CDATA[Avery]]>
</a>
<a name="EAN">
<![CDATA[9084692100225]]>
</a>
<a name="Kod producenta">
<![CDATA[AVSG710022]]>
</a>
</attrs>
For this I duplicate function:
// create+append "attrs" to "o"
$offer->appendChild(
$attrs = $document->createElement('attrs')
);
// create+append "a" to "attrs"
$attrs->appendChild(
$attr = $document->createElement('a')
);
// add properties to the "attrs" element
$attr->setAttribute('producent', $productBrand);
$attr->setAttribute('ean', $productEan);
$attr->setAttribute('kod_producent', $productSku);
But currently I've:
<attrs>
<a producent="4711" ean="4011700740031" kod_producent="12264"/>
</attrs>

You named the imgs element node variable $product and overwrite the o element - its parent node. Make sure to use semantic and unique names for your variables.
Use methods on the DOM document to create the nodes and append them to their parent node. appendChild() returns the appended node, so you can use it to chain calls and only need to store the node into a variable for extended modification.
Do not use the second argument of createElement(). It does not fully escape special characters. Set the textContent property or append a text node.
You can append the element node directly after creating it - here is no need to wait. You can modify the appended node at any time - this are the same methods you use to manipulate existing nodes.
Here is an example that creates your target XML structure (with reduced attributes).
$document = new DOMDocument('1.0', 'UTF-8');
// create+append the document element "offers"
$document->appendChild(
$offers = $document->createElement('offers')
);
// create+append the "o" element
$offers->appendChild(
$offer = $document->createElement('o')
);
// add properties to the "o" element
$offer->setAttribute('id', 'SKU123');
$offer->setAttribute('url', 'https://example.com#product');
// create+append the "name" to "o"
$offer
->appendChild($document->createElement('name'))
// chain call and set the text content
->textContent = 'Example product name';
// create+append "imgs" to "o"
$offer->appendChild(
$images = $document->createElement('imgs')
);
// create+append "main" to "imgs"
$images->appendChild(
$image = $document->createElement('main')
);
// add properties to the "main" element
$image->setAttribute('url', 'https://example.com#product-image');
$document->formatOutput = true;
echo $document->saveXML();
Output:
<?xml version="1.0" encoding="UTF-8"?>
<offers>
<o id="SKU123" url="https://example.com#product">
<name>Example product name</name>
<imgs>
<main url="https://example.com#product-image"/>
</imgs>
</o>
</offers>

Related

Remove child from XML with PHP DOM

I want to remove first video element (video src=time.mp4) from this xml (filename.xml) and save the xml into filename4.smil :
<?xml version="1.0" encoding="utf-8"?>
<smil>
<stream name="mysq"/>
<playlist name="Default" playOnStream="mysq" repeat="true" scheduled="2010-01-01 01:01:00">
<video src="time.mp4" start="0" length="-1"> </video>
<video src="sample.mp4" start="0" length="-1"> </video>
</playlist>
</smil>
i am using this code, but is not working:
<?php
$doc = new DOMDocument;
$doc->load("filename.xml");
$thedocument = $doc->documentElement;
//this gives you a list of the messages
$list0 = $thedocument->getElementsByTagName('playlist');
$list = $list0->item(0);
$nodeToRemove = null;
foreach ($list as $domElement){
$videos = $domElement->getElementsByTagName( 'video' );
$video = $videos->item(0);
$attrValue = $video->getAttribute('src');
if ($attrValue == 'time.mp4') {
$nodeToRemove = $videos; //will only remember last one- but this is just an example :)
}
}
//Now remove it.
if ($nodeToRemove != null)
$thedocument->removeChild($nodeToRemove);
$doc->save('filename4.smil');
?>
Assuming that there is only 1 playlist item and you want to remove the first video element from that, here are 2 methods.
This one uses getElementsByTagName() as you are in your code, but simple picks the first item from each list and then removes the item (you have to use parentNode to remove the child node).
$playlist = $doc->getElementsByTagName('playlist')->item(0);
$video = $playlist->getElementsByTagName( 'video' )->item(0);
$video->parentNode->removeChild($video);
This version uses XPath, which is more flexible, it looks for the playlist elements with a video element somewhere inside. Again, just taking the first one and removing it...
$xp = new DOMXPath($doc);
$video = $xp->query('//playlist//video')->item(0);
$video->parentNode->removeChild($video);
The problem with
$thedocument->removeChild($nodeToRemove);
is that you are trying to remove a child element from the base document. As this node is nested in the hierarchy, it won't be able to remove it, you need to remove it from it's direct parent.
Using Xpath expressions you can fetch video nodes with a specific src attribute, iterate them and remove them.
$document = new DOMDocument();
$document->loadXML($xml);
$xpath = new DOMXpath($document);
$expression = '/smil/playlist/video[#src="time.mp4"]';
foreach ($xpath->evaluate($expression) as $video) {
$video->parentNode->removeChild($video);
}
var_dump($document->saveXML());
It is possible to fetch nodes by position as well: /smil/playlist/video[1].

How to add nodes to a multi-level XML from an array?

I have an array $arr=array("A-B-C-D","A-B-E","A-B-C-F") and my expected output of the XML should be
<root>
<A>
<B>
<C>
<D></D>
<F></F>
</C>
<E></E>
</B>
</A>
</root>
I have already done the code that creates a new node of the XML for the first array element i.e. A-B-C-D. But when I move to the second element I need to check how many nodes are already created (A-B) and then add the new node based on that in the proper position.
So how do I traverse the XML and find the exact position where the new node should be attached?
my current code looks like this
$arr=explode("-",$input);
$doc = new DomDocument();
$doc->formatOutput=true;
$doc->LoadXML('<root/>');
$root = $doc->documentElement;
$comm = $doc->createElement('comm');
$root->appendChild($comm);
foreach($arr as $a2) {
$newcomm = $doc->createElement($a2);
$community->appendChild($newcomm);
$community=$newcomm;
}
Should I use xpath or some other method will be easier?
To stick with using DOMDocument, I've added an extra loop to allow you to add all of the original array items in. The main thing is before adding a new item in, check if it's already there...
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
$set=array("A-B-C-D","A-B-E","A-B-C-F-G", "A-B-G-Q");
$doc = new DomDocument();
$doc->formatOutput=true;
$doc->LoadXML('<root/>');
foreach ( $set as $input ) {
$arr=explode("-",$input);
$base = $doc->documentElement;
foreach($arr as $a2) {
$newcomm = null;
// Decide if the element already exists.
foreach ( $base->childNodes as $nextElement ) {
if ( $nextElement instanceof DOMElement
&& $nextElement->tagName == $a2 ) {
$newcomm = $nextElement;
}
}
if ( $newcomm == null ) {
$newcomm = $doc->createElement($a2);
$base->appendChild($newcomm);
}
$base=$newcomm;
}
}
echo $doc->saveXML();
As there is no quick way ( as far as I know) to check for a child with a specific tag name, it just looks through all of the child elements for a DOMElement with the same name.
I started using getElementByTagName, but this finds any child node with the name and not just at the current level.
The output from above is...
<?xml version="1.0"?>
<root>
<A>
<B>
<C>
<D/>
<F>
<G/>
</F>
</C>
<E/>
<G>
<Q/>
</G>
</B>
</A>
</root>
I added a few other items in to show that it adds things in at the right place.

Delete attribute in XML

I want to completely remove the size="id" attribute from every <door> element.
<?xml version="1.0" encoding="UTF-8"?>
<doors>
<door id="1" entry="3249" size="30"/>
<door id="1041" entry="6523" size="3094"/>
-- and 1000 more....
</doors>
The PHP code:
$xml = new SimpleXMLElement('http://mysite/doors.xml', NULL, TRUE);
$ids_to_delete = array( 1, 1506 );
foreach ($ids_to_delete as $id) {
$result = $xml->xpath( "//door[#size='$id']" );
foreach ( $result as $node ) {
$dom = dom_import_simplexml($node);
$dom->parentNode->removeChild($dom);
}
}
$xml->saveXml();
I get no errors but it does not delete the size attribute. Why?
I get no errors but it does not delete the size attribute. Why?
There are mulitple reasons why it does not delete the size attribute. The one that popped first into my mind was that attributes are no child nodes. Using a method to remove a child does just not fit to remove an attribute.
Each element node has an associated set of attribute nodes; the element is the parent of each of these attribute nodes; however, an attribute node is not a child of its parent element.
From: Attribute Nodes - XML Path Language (XPath), bold by me.
However, you don't see an error here, because the $result you have is an empty array. You just don't select any nodes to remove - neither elements nor attributes - with your xpath. That is because there is no such element you look for:
//door[#size='1']
You're searching for the id in the size attribute: No match.
These are the reasons why you get no errors and it does not delete any size attribute: 1.) you don't delete attributes here, 2.) you don't query any elements to delete attributes from.
How to delete attributes in SimpleXML queried by Xpath?
You can remove the attribute nodes by selecting them with an Xpath query and then unset the SimpleXMLElement self-reference:
// all size attributes of all doors
$result = $xml->xpath("//door/#size");
foreach ($result as $node) {
unset($node[0]);
}
In this example, all attribute nodes are queried by the Xpath expressions that are size attributes of door elements (which is what you ask for in your question) and then those are removed from the XML.
//door/#size
(see Abbreviated Syntax)
Now here the full example:
<?php
/**
* #link https://eval.in/215817
*/
$buffer = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<doors>
<door id="1" entry="3249" size="30"/>
<door id="1041" entry="6523" size="3094"/>
-- and 1000 more....
</doors>
XML;
$xml = new SimpleXMLElement($buffer);
// all size attributes of all doors
$result = $xml->xpath("//door/#size");
foreach ($result as $node) {
unset($node[0]);
}
$xml->saveXml("php://output");
Output (Online Demo):
<?xml version="1.0" encoding="UTF-8"?>
<doors>
<door id="1" entry="3249"/>
<door id="1041" entry="6523"/>
-- and 1000 more....
</doors>
You can do your whole query in DOMDocument using DOMXPath, rather than switching between SimpleXML and DOM:
$dom = new DOMDocument;
$dom->load('my_xml_file.xml');
# initialise an XPath object to act on the $dom object
$xp = new DOMXPath( $dom );
# run the query
foreach ($xp->query( "//door[#size]" ) as $door) {
# remove the attribute
$door->removeAttribute('size');
}
print $dom->saveXML();
Output for the input you supplied:
<?xml version="1.0" encoding="UTF-8"?>
<doors>
<door id="1" entry="3249"/>
<door id="1041" entry="6523"/>
</doors>
If you do want only to remove the size attribute for the IDs in your list, you should use the code:
foreach ($ids_to_delete as $id) {
# searches for elements with a matching ID and a size attribute
foreach ($xp->query("//door[#id='$id' and #size]") as $door) {
$door->removeAttribute('size');
}
}
Your code wasn't working for several reasons:
it looks like your XPath was wrong, since your array is called $ids_to_delete and your XPATH is looking for door elements with the size attribute equal to the value from $ids_to_delete;
you're converting the nodes to DOMDocument objects ($dom = dom_import_simplexml($node);) to do the deletion, but $xml->saveXml();, which I presume you printed somehow, is a SimpleXML object;
you need to remove the element attribute; removeChild removes the whole element.

appendChild using DOMDocument/PHP/XML

I am trying to update my XML file based on an HTML form processed by PHP but the new XML snippet I am trying to append to specific areas of my current XML just keeps getting added to the end of my document.
$specific_node = "0"; //this is normally set by a select input from the form.
$doc = new DOMDocument();
$doc->load( 'rfp_files.xml' );
$doc->formatOutput = true;
//below is where my issue is having problems the variable '$specific_node' can be one of three options 0,1,2 and what I am trying to do is find the child of content_sets. So the first second or third child elemts and that is where I will add my new bit of XML
$r = $doc->getElementsByTagname('content_sets')->item($specific_node);
//This is where I build out my new XML to append
$fileName = $doc->createElement("file_name");
$fileName->appendChild(
$doc->createTextNode( $Document_Array["url"] )
);
$b->appendChild( $fileName );
//this is were I add the new XML to the child node mention earlier in the script.
$r->appendChild( $b );
XML Example:
<?xml version="1.0" encoding="UTF-8"?>
<content_sets>
<doc_types>
<article>
<doc_name>Additional</doc_name>
<file_name>Additional.docx</file_name>
<doc_description>Test Word document. Please remove when live.</doc_description>
<doc_tags>word document,test,rfp template,template,rfp</doc_tags>
<last_update>01/26/2013 23:07</last_update>
</article>
</doc_types>
<video_types>
<article>
<doc_name>Test Video</doc_name>
<file_name>test_video.avi</file_name>
<doc_description>Test video. Please remove when live.</doc_description>
<doc_tags>test video,video, avi,xvid,svid avi</doc_tags>
<last_update>01/26/2013 23:07</last_update>
</article>
</video_types>
<image_types>
<article>
<doc_name>Test Image</doc_name>
<file_name>logo.png</file_name>
<doc_description>Logo transparent background. Please remove when live.</doc_description>
<doc_tags>png,logo,logo png,no background,graphic,hi res</doc_tags>
<last_update>01/26/2013 23:07</last_update>
</article>
</image_types>
</content_sets>
This is getting the root element:
$specific_node = "0";
$r = $doc->getElementsByTagname('content_sets')->item($specific_node);
So you are appending a child onto the root which is why you always see it added near the end of the document. You need to get the children of the root element like this:
$children = $doc->documentElement->childNodes;
This can return several types of node, but you are only interested in 'element' type nodes. It's not very elegant, but the only way I've found to get a child element by position is looping like this...
$j = 0;
foreach ($doc->documentElement->childNodes as $r)
if ($r->nodeType === XML_ELEMENT_NODE && $j++ == $specific_node)
break;
if ($j <= $specific_node)
// handle situation where $specific_node is more than number of elements
You could use getElementsByTagName() if you can pass the name of the node required instead of the ordinal position, or change the XML so that the child elements all have the same name and use an attribute to differentiate them.

Creating XML Fragment in domdocument and adding to its parent

Below is the piece of code that I am using to create a bunch of xml nodes and trying to attach to parent that is passed to it.
Not sure whats going wrong, Parent node is not getting augmented.
input.xml
<?xml version="1.0"?>
<root>
<steps> This is a step </steps>
<steps> This is also a step </steps>
</root>
output.xml should be
<?xml version="1.0">
<root>
<steps>
<step>
<step_number>1</step_number>
<step_detail>Step detail goes here</step_detail>
</step>
</steps>
<steps>
<step>
<step_number>2</step_number>
<step_detail>Step detail of the 2nd step</step_detail>
</step>
</steps>
</root>
<?php
$xml = simplexml_load_file( 'input.xml' );
$domxml = dom_import_simplexml( $xml );
//Iterating through all the STEPS node
foreach ($steps as $stp ){
createInnerSteps( &$stp, $stp->nodeValue , 'Expected Result STring' );
echo 'NODE VAL :-----' . $stp->nodeValue;// Here it should have the new nodes, but it is not
}
function createInnerSteps( &$parent )
{
$dom = new DOMDocument();
// Create Fragment, where all the inner childs gets appended
$frag = $dom->createDocumentFragment();
// Create Step Node
$stepElm = $dom->createElement('step');
$frag->appendChild( $stepElm );
// Create Step_Number Node and CDATA Section
$stepNumElm = $dom->createElement('step_number');
$cdata = $dom->createCDATASection('1');
$stepNumElm->appendChild ($cdata );
// Append Step_number to Step Node
$stepElm->appendChild( $stepNumElm );
// Create step_details and append to Step Node
$step_details = $dom->createElement('step_details');
$cdata = $dom->createCDATASection('Details');
$step_details->appendChild( $cdata );
// Append step_details to step Node
$stepElm->appendChild( $step_details );
// Add Parent Node to step element
// I get error PHP Fatal error:
// Uncaught exception 'DOMException' with message 'Wrong Document Error'
$parent->appendChild( $frag );
//echo 'PARENT : ' .$parent->saveXML();
}
?>
You are getting the Wrong Document Error because you create a new DOMDocument object within createInnerSteps(), each time it is called.
When the code reaches the line $parent->appendChild($frag), the $frag belongs to the document created in the function and the $parent belongs to the main document that you are trying to manipulate. You cannot move nodes (or fragments) between documents like this.
The simplest solution is to only ever use the one document by replacing:
$dom = new DOMDocument();
with
$dom = $parent->ownerDocument;
See a running example.
Finally, to get your desired output, you will need to remove the existing text within each <steps> element.

Categories