I have been trying to make a method for adding new elements in an XML file, but for some reason it does not modify the element, even that when i Use saveXML prints the right ones.
function fnDOMEditElementCond($product_id, $name, $weight, $category, $location) {
if (!isset($product_id) || is_numeric($product_id)) {
return false;
}
$obj = new helperClass();
$xmlDoc = $obj->fetchFromXMLDocument('storage.xml');
$xpath = new DOMXPath($xmlDoc);
$result = $xpath->query(sprintf('/storagehouse/item[#id="%s"]', $product_id));
if (!$result || $result->length !== 1) {
throw new Exception(sprintf('Item with id "%s" does not exists or is not unique.', $product_id));
}
$item = $result->item(0);
//Change the name element
$xName = $xpath->query("./name", $item)->item(0);
$xName->removeChild($xName);
// //Change the name element
// $xWeight = $xpath->query("./weight", $item)->item(0);
// $xWeight->nodeValue = $weight;
//
// //Change the name element
// $xLocation = $xpath->query("./location", $item)->item(0);
// $xLocation->nodeValue = $category;
//
// //Change the name element
// $xCategory = $xpath->query("./category", $item)->item(0);
// $xCategory->nodeValue = $location;
echo $xmlDoc->saveXML($item);
}
}
This code takes a ID, and depending on the id it modifies the information, on four elements.
Here is the xml fil
storagehouse xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="schema.xsd">
<item id="c7278e33ef0f4aff88da10dfeeaaae7a">
<name>HDMI Cable 3m</name>
<weight>0.5</weight>
<category>Cables</category>
<location>B3</location>
</item>
<item id="df799fb47bc1e13f3e1c8b04ebd16a96">
<name>Dell U2410</name>
<weight>2.5</weight>
<category>Monitors</category>
<location>C2</location>
</item>
<item id="53abbd89766ea8759b5ebe5bacd43f58">
<name>HP Probook 1311</name>
<weight>2.1</weight>
<category>Notebooks</category>
<location>A1</location>
</item>
<storagehouse/>
Do you have any idea on why that it might not working?
I believe it is because you are looking for save() not savexml(). And you will need to give save() the filename.
Edit: Also, if I'm remembering correctly, you will need to reload the DOM after each save, otherwise you will only effect the unsaved DOM elements. But it's been a while since I've had to do it. I'll take a look through my old code to double check.
Edit Again: I've been looking through my code, and it does appear to be necessary. You can do all the modifications you need at once, but if you need to view the results in the interim, you either have to produce it with saveXML() or save() it and read it into a fresh XML object. The original object will only reflect the original content. I didn't properly note it in my comments like I should have, but my code is done in such a way that it makes me think I am right. Hopefully this helps.
Related
I am trying to add an array $item to an XML file in order to then be able to read all of the items in a later time.
I have the following PHP to perform this action:
<?php
$item = array();
$item['rating'] = $_GET['rating'];
$item['comment'] = $_GET['comment'];
$item['item_id'] = $_GET['item_id'];
$item['status'] = "pending";
//Defining $xml
$xml = new SimpleXMLElement('<root/>');
array_walk_recursive($item, array($xml, 'addChild'));
$xml = $xml->asXML();
$dom = new DOMDocument;
$dom->preserveWhiteSpace = FALSE;
$dom->loadXML($xml);
//Save XML as a file
$dom->save('reviews.xml');
However, when run what I get this in my XML file:
< ?xml version="1.0"?>
Basically my array is no where to be seen.
A var_dump of $item gives
array(4) { ["rating"]=> string(1) "8" ["comment"]=> string(17) "I Really Like it!" ["item_id"]=> string(1) "9" ["status"]=> string(7) "pending" }
How could I modify my code in order to have it save an array (and if there are many keep them all) in the file reviews.xml?
Also How could I make it so that later on I would be able to access the data; for instance changing the status from pending to approved?
EDIT:
Using the following code I have been able to save my item to the file:
$item = array();
$item[$_GET['rating']] = 'rating';
$item[$_GET['comment']] = 'comment';
$item[$_GET['item_id']] = 'item_id';
$item['pending'] = 'status';
$xml = new SimpleXMLElement('<root/>');
array_walk_recursive($item, array($xml, 'addChild'));
$xml->asXML('reviews.xml');
However I am still unable to append new data to the root rather than overwriting the current saved data.
As I was saying in my comment... The code you provided errors with WARNING DOMDocument::loadXML(): Empty string supplied as input. You never assigned anything to $xml'...
Proper error reporting/logging would help spot these mistakes.
<?php
$item = array();
$item['rating'] = 'a';
$item['comment'] = 'b';
$item['item_id'] = 'c';
$item['status'] = "pending";
//Defining $xml
$xml = new SimpleXMLElement('<root/>');
array_walk_recursive($item, array($xml, 'addChild'));
//THIS IS THE LINE YOU WERE MISSING
$xml = $xml->asXML();
$dom = new DOMDocument;
$dom->preserveWhiteSpace = FALSE;
$dom->loadXML($xml);
//Save XML as a file
$dom->save('reviews.xml');
If you echoed it out...
var_dump($dom->saveHTML());
> string(80)
> "<root><a>rating</a><b>comment</b><c>item_id</c><pending>status</pending></root>"
Please avoid updating your existing question with additional questions.
A database would make the task easier. Using a flat file works fine though, XML, or some other format. You will need to be able to retrieve a record by item_id, at which point you modify it, then replace it. That is the gist of it.
So here's an overhaul of your code, with some changes to both your approach and the scheme of your XML, based on your various comments and updates.
So first, instead of creating XML that looks like this:
<root>
<rating>a</rating>
<comment>b</comment>
<item_id>c</item_id>
<status>pending</status>
</root>
You're going to store the XML like this:
<root>
<item id="c">
<rating>a</rating>
<comment>b</comment>
<status>pending</status>
</item>
</root>
This is based on a few of your comments:
You are wanting to add to the XML file rather than overwrite the existing file content. That suggests that you want to store multiple items. This would also explain why you have a property item_id. So rather than having a mess of XML like :
<root>
<rating>a</rating>
<comment>b</comment>
<item_id>c</item_id>
<status>pending</status>
<rating>d</rating>
<comment>e</comment>
<item_id>f</item_id>
<status>pending</status>
<rating>g</rating>
<comment>h</comment>
<item_id>i</item_id>
<status>pending</status>
</root>
where it is impossible to know which item is which, you store each set of item properties on an <item> element. Since you are going to want to easily grab an item based on its item_id in order to update that item, making item_id an attribute of the <item> makes more sense than making it a child of the <item>.
You want to be able to update the status. This is where having the item_id stored on the item comes in handy. If someone submits a request with an existing item_id, you can update that item, including its status element. Or you could do it whenever you need to from some other process, etc.
Here's the code I drummed up for this. Note that it currently isn't set up to look for an existing element with that item id, but that should be possible using existing SimpleXML functions/methods.
$item = array();
$item_id = "c";
$item['rating'] = 'a';
$item['comment'] = 'b';
$item['status'] = "pending";
$xml = simplexml_load_file('ratings.xml');
//if ratings.xml not found or not valid xml, create clean XML with <root/>
if($xml === false) {
$xml = new SimpleXMLElement('<root/>');
}
$xml_item = $xml->addChild("item");
$xml_item->addAttribute("id", $item_id);
foreach($item as $name => $value) {
$xml_item->addChild($name, $value);
}
$xml->asXML('ratings.xml');
Notice that one of the major changes I made to your existing code is changing from using array_walk_recursive to a simple foreach. array_walk_recursive for this purpose is a short cut that causes more issues than it solves. For instance, you had to swap your key and value on the $item array, which is confusing. It also isn't necessary for what you currently are doing, since you don't have a multi-dimensional array. And even if you did, array_walk_recursive isn't the right choice to handle looping over the array recursively because it would add each array member to the root of the XML, not add sub-arrays as children of their parent entry as they show up in the actual array. Point being, it's confusing, it doesn't add any value, and using a foreach is a lot more clear on what you are actually doing.
I've also changed
$item['item_id'] = 'c';
to
$item_id = 'c';
and then added it to the item element as an attribute like:
$xml_item->addAttribute("id", $item_id);
This is consistent with the new schema I outlined earlier.
Finally, instead of passing the XML to DOMDocument, I'm just using
$xml->asXML('ratings.xml');
SimpleXML already removes any extra whitespace, so there is no need to use DOMDocument to achieve this.
Based on some of the counterintuitive parts of your original code, it looks like you may have done a decent amount of copy and pasting to get it going. Which is where most of us start, but it's a good idea to be upfront about things like "I don't understand quite what this code is doing, I just grabbed it from a script that did some of what I need." It will save us all a lot of time and grief if we're not assuming you are using the code you have because you need to or it was a conscious decision, etc, and that we have to work within the constraints of that code.
I hope this gets you off to a good start.
Update
I was messing around with it, and came up with the following for updating existing <item> if an item with id set to $item_id already exists. It's a bit clunky, but it tested and it works.
This assumes the $item_id and $item array get set as normal, as well as retrieving the exiting XML, as covered above. I'm providing the lines just before the changes for reference:
$xml = simplexml_load_file('ratings.xml');
//if ratings.xml not found or not valid xml, create clean XML with <root/>
if($xml === false) {
$xml = new SimpleXMLElement('<root/>');
}
//query with xpath for existing item with $item_id
$item_with_id = $xml->xpath("/root/item[#id='{$item_id}']");
// if the xpath returns a result, update that item with new values.
if(count($item_with_id) > 0) {
$xml_item = $item_with_id[0];
foreach($item as $name => $value) {
$xml_item->$name = $value;
}
} else {
// if the xpath returns no results, create new item element.
$xml_item = $xml->addChild("item");
$xml_item->addAttribute("id", $item_id);
foreach($item as $name => $value) {
$xml_item->addChild($name, $value);
}
}
I happen to be unfortunate enough to be working with an api that has images on the same XML tag level as the other tags and have the subscripts i.e 1,2,3,4 as part of the tag name of the image. Total images of each vehicle will vary in count.
<Vehicle>
<TITLE>Some car name i dont need</TITLE>
<DESCRIPTION>Some description i also dont need</DESCRIPTION>
<IMAGE_URL1>{imagelinkhere i want}</IMAGE_URL1>
<IMAGE_URL2>{imagelinkhere i want}</IMAGE_URL2>
<IMAGE_URL3>{imagelinkhere i want}</IMAGE_URL3>
<IMAGE_URL4>{imagelinkhere i want}</IMAGE_URL4>
</Vehicle>
I am using PHP's method simplexml_load_file(xml_url) to parse the entire xml into an object array.
My question: Is there a way to get these images using the same method which is also efficient and clean?
EDIT:
I have just refined the xml to show that there are other tags i dont need there and already handling.
$xml = '<Vehicle>
<DESCRIPTION/>
<IMAGE_URL1>{imagelinkhere}</IMAGE_URL1>
<IMAGE_URL2>{imagelinkhere}</IMAGE_URL2>
<IMAGE_URL3>{imagelinkhere}</IMAGE_URL3>
<IMAGE_URL4>{imagelinkhere}</IMAGE_URL4>
</Vehicle>';
$parsed = simplexml_load_string($xml);
If you know, that the image url tags will always contain the name IMAGE_URL, you can check them:
foreach ($parsed as $key => $image) {
if (strpos($key, 'IMAGE_URL') !== false) {
echo $image, '</br>';
}
}
You can fetch the nodes with Xpath.
$xml = <<<'XML'
<Vehicle>
<TITLE>Some car name i dont need</TITLE>
<DESCRIPTION>Some description i also dont need</DESCRIPTION>
<IMAGE_URL1>image1</IMAGE_URL1>
<IMAGE_URL2>image2</IMAGE_URL2>
<IMAGE_URL3>image3</IMAGE_URL3>
<IMAGE_URL4>image4</IMAGE_URL4>
</Vehicle>
XML;
$vehicle = new SimpleXMLElement($xml);
foreach ($vehicle->xpath('*[starts-with(local-name(), "IMAGE_URL")]') as $imageUrl) {
var_dump((string)$imageUrl);
}
Output:
string(6) "image1"
string(6) "image2"
string(6) "image3"
string(6) "image4"
* selects all element child nodes. [] is a condition. In this case a validation that the local name (tag name without any namespace prefix) starts with a specific string.
This looks not that much different in DOM. But you start at the document context.
$document = new DOMDocument();
$document->loadXml($xml);
$xpath = new DOMXpath($document);
foreach ($xpath->evaluate('/Vehicle/*[starts-with(local-name(), "IMAGE_URL")]') as $imageUrl) {
var_dump($imageUrl->textContent);
}
I have an xml file that is created thru php. The basic structure is:
<catgories> *this is root*
<category> *element*
<title>xy</title> *attribute*
<desc>yu</desc> *attribute*
<categoryLeaf> *child element of category*
<title>rt</title> *attribute of categoryLeaf*
</categoryLeaf>
<endtvod></endtvod>
</category>
</categories>
The xml is created on the fly at one time with the exception of the categoryLeaf and it's attributes which are created in a separate function. The xml is created correctly. In order to get the categoryLeaf I am parsing an html and extracting information and storing that information in an array with the follwoing:
if (!empty($dom)) { //IF NOT EMPTY FIND CATEGORIES
$clsort = $xpath->query('//div[#class="sort"]');
foreach($clsort as $clsorts){
$li= $clsorts->getElementsByTagName('li');
foreach ($li as $lis){
$links=$lis->getElementsByTagname('a');
foreach ($links as $link){
$href=$link->getAttribute('href');
$text=$link->nodeValue;
if ($text=="#"){
break;
}else{
$textarray=$text;
ECHO $textarray."<br>";
CategoryLeafXml($textarray);
}
}
}
}
}
else
{
echo "EMPTY CONTAINER". "<br>";
}
The desired information is obtained thru the code above and added to the textarray. I can echo it and each shows up. I pass the array to a function that is to open the xml, insert the categoryLeaf along with the title attribute and save thru code:
Function CategoryLeafXml($textarray){
$xml = new DOMDocument('1.0', 'UTF-8');
$xml->formatOutput = true;
$xpath = new DOMXPath($xml);
$xml-> loadXML ('.\\xml\\test.xml');
$arrcount=count($textarray);
echo $arrcount."<br>";
$categoryLeaf=$xml->createElement("categoryLeaf");
for ($i=0; $i<$arrcount;$i++){
echo $textarray."<br>";
$endtvod=$xml->getElementsByTagName('endtvod')->item(0); //LOCATE tag
$category=$xml->getElementsByTagName('category');
$categories=$xml->getElementsByTagName('categories');
$newleaf=$xml->insertBefore($categoryLeaf, $endtvod);
$title = $xml->createAttribute("title");
$title->value= $textarray;
$categoryLeaf->appendChild($title);
$xml->save('.\\xml\\test.xml');
}
}
What is suppose to occur is the xml structure is created, the html is parse into the textarray, the textarray is passed to the function where the element categoryLeaf is inserted before the element tvod. The textarray is the attribute value for the categoryLeaf/title. I am having 2 problems, (1)the xml is opened and the categoryLeaf is created, but when saved the result is:
<?xml version="1.0" encoding="UTF-8"?>
<categoryLeaf title="value of textarray"/>
The xml is overwritten leaving only the one inserted element with only the last array value.
(2) The array count always shows 1 and the for loop only runs one time writing only the array's last value. I anticipated a categoryLeaf being added for value of the array count but it is only seeing that value as being one. I know there are about 25 entries in the array.
You are only creating a single categoryLeaf node through:
$categoryLeaf=$xml->createElement("categoryLeaf")
and changing the attribute n times.
You have to create the categoryLeaf inside the for loop.
I also do not understand why you are doing this:
$category=$xml->getElementsByTagName('category');
$categories=$xml->getElementsByTagName('categories');
That does not seem to do anything inside the function.
Just try something like this:
Function CategoryLeafXml($textarray){
$xml = new DOMDocument('1.0', 'UTF-8');
$xml->formatOutput = true;
$xpath = new DOMXPath($xml);
$xml-> loadXML ('.\\xml\\test.xml');
$endtvod = $xml->getElementsbyTagName('endtvod')->item(0)
foreach ($textarray as $text){
$categoryLeaf=$xml->createElement("categoryLeaf");
$categoryLeaf->setAttribute('title', $text);
$endtvod->parentNode->insertBefore($categoryLeaf, $enttvod)
}
$xml->save('.\\xml\\test.xml');
}
Ok, please bear with me as I explain this app. The first view shows a tableview, where each row is a 'request' that someone has made, also using the app. The requests are actually a parsed xml file that looks something like this:
<item>
<title>Request</title>
<name>Name</name>
<Responses>0</Responses>
</item>
Using the script listed underneath this paragraph, I am able to run some code in iOS that will take text from text-boxes in the app, and append the XML with a new item set.
<?php
$firstName = $_POST['firstName'];
$lastName = $_POST['lastName'];
$request = $_POST['request'];
$anon = $_POST['anon'];
$pubDate = $_POST['pubDate'];
$loc = $_POST['loc'];
//This line will load the XML file
$xml = simplexml_load_file("URL.xml") or die("Not loaded!\n");
//print_r($xml);
//This line gets the channel element (from an array returned by the xpath method)
$channel = $xml->xpath('//channel');
$channel = $channel[0];
//print_r($channel);
$person = $channel->addChild("item");
$person->addChild("first_name", $firstName);
$person->addChild("last_name", $lastName);
$person->addChild("title", $request);
$person->addChild("date", $pubDate);
$person->addChild("anonymous", $anon);
$person->addChild("responses", "0");
$person->addChild("location", $loc);
//This next line will overwrite the original XML file with new data added
$xml->asXML("Test.xml");
?>
The actual XML has much more listed than what I put at the beginning, I just edited it down to save space, so that's why it and the PHP don't quite match up.
All of this works great, the app parses the XML to show what has been requested, and a user can make a new request that will then show in the app.
My problem is that when someone responds to an existing request, I would like the <Responses> to increase by 1. Is there something I can do with a different script that would find that entry, and +1 to whatever number currently exists in that item's Response tag?
I have seen this script, but it appears to go through and search for all of a certain tag, and change all tags of that name in the XML. I am looking to just find one specific one and change it. Suggestions?
$str = <<<XML
<ScrapBook>
<Event>
<Name> Name of Event </Name>
<Blah>glop glop</Blah>
</Event>
</ScrapBook>
XML;
$xmlDoc = new DOMDocument();
$xmlDoc->loadXml($str);
$events = $xmlDoc->getElementsByTagName("Event");
foreach($events as $event){
$eventNames = $event->getElementsByTagName("Name");
$eventN = $eventNames->item(0)->nodeValue;
if(' Name of Event ' == $eventN){
$eventNames->item(0)->nodeValue = 'New name';
}
}
var_dump($xmlDoc->saveXML());
You can go with simplexml to do that; first, select the <item> to be changed, then store the new <Responses>:
$xml = simplexml_load_string($x); // assume XML in $x
// select <Responses> with xpath by title and name
$responses = $xml->xpath("/items/item[title = 'Foo' and name = 'Bar']/Responses");
// store number + 1
$responses[0][0] = $responses[0] + 1;
see it working: https://eval.in/127882
Hello I know there is many questions here about those three topics combined together to update XML entries, but it seems everyone is very specific to a given problem.
I have been spending some time trying to understand XPath and its way, but I still can't get what I need to do.
Here we go
I have this XML file
<?xml version="1.0" encoding="UTF-8"?>
<storagehouse xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="schema.xsd">
<item id="c7278e33ef0f4aff88da10dfeeaaae7a">
<name>HDMI Cable 3m</name>
<weight>0.5</weight>
<category>Cables</category>
<location>B3</location>
</item>
<item id="df799fb47bc1e13f3e1c8b04ebd16a96">
<name>Dell U2410</name>
<weight>2.5</weight>
<category>Monitors</category>
<location>C2</location>
</item>
</storagehouse>
What I would like to do is to update/edit any of the nodes above when I need to. I will do a Html form for that.
But my biggest conserne is how do I find and update a the desired node and update it?
Here I have some of what I am trying to do
<?php
function fnDOMEditElementCond()
{
$dom = new DOMDocument();
$dom->load('storage.xml');
$library = $dom->documentElement;
$xpath = new DOMXPath($dom);
// I kind of understand this one here
$result = $xpath->query('/storagehouse/item[1]/name');
//This one not so much
$result->item(0)->nodeValue .= ' Series';
// This will remove the CDATA property of the element.
//To retain it, delete this element (see delete eg) & recreate it with CDATA (see create xml eg).
//2nd Way
//$result = $xpath->query('/library/book[author="J.R.R.Tolkein"]');
// $result->item(0)->getElementsByTagName('title')->item(0)->nodeValue .= ' Series';
header("Content-type: text/xml");
echo $dom->saveXML();
}
?>
Could someone maybe give me an examples with attributes and so on, so one a user decides to update a desired node, I could find that node with XPath and then update it?
The following example is making use of simplexml which is a close friend of DOMDocument. The xpath shown is the same regardless which method you use, and I use simplexml here to keep the code low. I'll show a more advanced DOMDocument example later on.
So about the xpath: How to find the node and update it. First of all how to find the node:
The node has the element/tagname item. You are looking for it inside the storagehouse element, which is the root element of your XML document. All item elements in your document are expressed like this in xpath:
/storagehouse/item
From the root, first storagehouse, then item. Divided with /. You already know that, so the interesting part is how to only take those item elements that have the specific ID. For that the predicate is used and added at the end:
/storagehouse/item[#id="id"]
This will return all item elements again, but this time only those which have the attribute id with the value id (string). For example in your case with the following XML:
$xml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<storagehouse xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="schema.xsd">
<item id="c7278e33ef0f4aff88da10dfeeaaae7a">
<name>HDMI Cable 3m</name>
<weight>0.5</weight>
<category>Cables</category>
<location>B3</location>
</item>
<item id="df799fb47bc1e13f3e1c8b04ebd16a96">
<name>Dell U2410</name>
<weight>2.5</weight>
<category>Monitors</category>
<location>C2</location>
</item>
</storagehouse>
XML;
that xpath:
/storagehouse/item[#id="df799fb47bc1e13f3e1c8b04ebd16a96"]
will return the computer monitor (because such an item with that id exists). If there would be multiple items with the same id value, multiple would be returned. If there were none, none would be returned. So let's wrap that into a code-example:
$simplexml = simplexml_load_string($xml);
$result = $simplexml->xpath(sprintf('/storagehouse/item[#id="%s"]', $id));
if (!$result || count($result) !== 1) {
throw new Exception(sprintf('Item with id "%s" does not exists or is not unique.', $id));
}
list($item) = $result;
In this example, $titem is the SimpleXMLElement object of that computer monitor xml element name item.
So now for the changes, which are extremely easy with SimpleXML in your case:
$item->category = 'LCD Monitor';
And to finally see the result:
echo $simplexml->asXML();
Yes that's all with SimpleXML in your case.
If you want to do this with DOMDocument, it works quite similar. However, for updating an element's value, you need to access the child element of that item as well. Let's see the following example which first of all fetches the item as well. If you compare with the SimpleXML example above, you can see that things not really differ:
$doc = new DOMDocument();
$doc->loadXML($xml);
$xpath = new DOMXPath($doc);
$result = $xpath->query(sprintf('/storagehouse/item[#id="%s"]', $id));
if (!$result || $result->length !== 1) {
throw new Exception(sprintf('Item with id "%s" does not exists or is not unique.', $id));
}
$item = $result->item(0);
Again, $item contains the item XML element of the computer monitor. But this time as a DOMElement. To modify the category element in there (or more precisely it's nodeValue), that children needs to be obtained first. You can do this again with xpath, but this time with an expression relative to the $item element:
./category
Assuming that there always is a category child-element in the item element, this could be written as such:
$category = $xpath->query('./category', $item)->item(0);
$category does now contain the first category child element of $item. What's left is updating the value of it:
$category->nodeValue = "LCD Monitor";
And to finally see the result:
echo $doc->saveXML();
And that's it. Whether you choose SimpleXML or DOMDocument, that depends on your needs. You can even switch between both. You probably might want to map and check for changes:
$repository = new Repository($xml);
$item = $repository->getItemByID($id);
$item->category = 'LCD Monitor';
$repository->saveChanges();
echo $repository->getXML();
Naturally this requires more code, which is too much for this answer.