Remove node on XML dom doc by selecting value - php

Trying to make an API for currency conversion,
Need to select a specific currency and delete it from the xml file...
XML file looks like this:
<currencies>
<currency>
<ccode>CAD</ccode>
<cname>Canadian Dollar</cname>
<cntry>Canada</cntry>
</currency>
<currency>
<ccode>CHF</ccode>
<cname>Swiss Franc</cname>
<cntry>Liechtenstein, Switzerland</cntry>
</currency>
<currency>
<ccode>CNY</ccode>
<cname>Yuan Renminbi</cname>
<cntry>China</cntry>
</currency>
...etc
I need to use php to select and delete the specific currency, at the moment trying this:
<?php
$dom = new DOMDocument("1.0", "utf-8");
$dom->load('data/ccodes.xml');
$nodes = $dom->getElementsByTagName("currencies");
foreach ($nodes as $n){
if($n->getAttribute("ccode") == "CAD") {
$parent = $n->parentNode;
$parent->removeChild($n);
}
}
echo $dom->saveXML();
?>
But It's not working.... I'm pretty sure it's really simple but I have no idea what I'm doing with coding... :/
Need to make it so I can just change CAD to whatever to delete any currency I need to...

Your iterating the root node currencies but I think you meant to iterate the currency nodes. ccode is not an attribute node, but a child element node. Even if you iterate currency nodes with the correct condition it would still not fully work.
DOMElement::getElementsByTagName() returns a live result. Inside the loop you modify the DOM and the list is modified as well. You could us a for loop to iterate it backwards, use iterator_to_array() to materialize the node list into an array or use Xpath. DOMXpath::evaluate() returns a node list, but it is not a live result. So the list will not change if you modify the document.
$document = new DOMDocument();
//$document->load('data/ccodes.xml');
$document->loadXml($xml);
$xpath = new DOMXpath($document);
foreach ($xpath->evaluate('/currencies/currency[ccode="CAD"]') as $node) {
$node->parentNode->removeChild($node);
}
echo $document->saveXML();
Output:
<?xml version="1.0"?>
<currencies>
<currency>
<ccode>CHF</ccode>
<cname>Swiss Franc</cname>
<cntry>Liechtenstein, Switzerland</cntry>
</currency>
<currency>
<ccode>CNY</ccode>
<cname>Yuan Renminbi</cname>
<cntry>China</cntry>
</currency>
</currencies>

Related

XML: Delete parent with simplexml_import_dom (PHP)

I want to delete those entries where the title matches my $titleArray.
My XML files looks like:
<products>
<product>
<title>Battlefield 1</title>
<url>https://www.google.de/</url>
<price>0.80</price>
</product>
<product>
<title>Battlefield 2</title>
<url>https://www.google.de/</url>
<price>180</price>
</product>
</products>
Here is my code but I don't think that it is working and my IDE says here $node->removeChild($product); -> "Expected DOMNode, got DOMNodeList"
What is wrong and how can I fix that?
function removeProduct($dom, $productTag, $pathXML, $titleArray){
$doc = simplexml_import_dom($dom);
$items = $doc->xpath($pathXML);
foreach ($items as $item) {
$node = dom_import_simplexml($item);
foreach ($titleArray as $title) {
if (mb_stripos($node->textContent, $title) !== false) {
$product = $node->parentNode->getElementsByTagName($productTag);
$node->removeChild($product);
}
}
}
}
Thank you and Greetings!
Most DOM methods that fetch nodes return a list of nodes. You can have several element nodes with the same name. So the result will a list (and empty list if nothing is found). You can traverse the list and apply logic to each node in the list.
Here are two problems with the approach. Removing nodes modifies the document. So you have to be careful not to remove a node that you're still using after that. It can lead to any kind of unexpected results. DOMNode::getElementsByTagName() returns a node list and it is a "live" result. If you remove the first node the list actually changes, not just the XML document.
DOMXpath::evaluate() solves two of the problems at the same time. The result is not "live" so you can iterate the result with foreach() and remove nodes. Xpath expressions allow for conditions so you can filter and fetch specific nodes. Unfortunately Xpath 1.0 has now lower case methods, but you can call back into PHP for that.
function isTitleInArray($title) {
$titles = [
'battlefield 2'
];
return in_array(mb_strtolower($title, 'UTF-8'), $titles);
}
$document = new DOMDocument();
$document->loadXml($xml);
$xpath = new DOMXpath($document);
$xpath->registerNamespace("php", "http://php.net/xpath");
$xpath->registerPHPFunctions('isTitleInArray');
$expression = '//product[php:function("isTitleInArray", normalize-space(title))]';
foreach ($xpath->evaluate($expression) as $product) {
$product->parentNode->removeChild($product);
}
echo $document->saveXml();
Output:
<?xml version="1.0"?>
<products>
<product>
<title>Battlefield 1</title>
<url>https://www.google.de/</url>
<price>0.80</price>
</product>
</products>

Parse XML with tags not grouped under one tag, but really should be

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);
}

xPath select attribute based on other attribute's value

I'm struggling with xPath for a while now and i thought i'd got the hang of it until now.
The strange thing is that if i test my pattern online it works, but when i run it locally it doesn't
I have the following XML
<Products>
<Product>
<Property Title="Toy" Text="Car" />
</Product>
</Products>
Now i want to replace all Car values with Bal so i came up with something like this:
$xml_src = 'feed.xml';
$document = new DOMDocument();
$document->load($xml_src);
$xpath = new DOMXpath($document);
foreach($xpath->evaluate('//Property[#Title="Toy"]/#Text') as $text){
$text->data = str_replace('Car', 'Bal', $text->data);
}
echo $document->saveXml();
But that doesn't do anything (i just get the whole feed with the original values), while the xPath pattern works on the site i mentioned above. I don't have a clue why
Your Xpath expression returns DOMAttr nodes. You will have to manipulate the $value property.
foreach($xpath->evaluate('//Property[#Title="Toy"]/#Text') as $text) {
$text->value = str_replace('Car', 'Bal', $text->value);
}
echo $document->saveXml();
A DOMAttr has a single child node that is a DOMText representing its value, but I don't think it is possible to address it with Xpath. Text nodes would be instances of DOMText, they would have a $data property.
You'd have to manuipulate the DOM, not just the representation, which in this case means using DOMElement::setAttribute
foreach($xpath->evaluate('//Property[#Title="Toy"]') as $elProperty) {
$elProperty->setAttribute(
'Text',
str_replace(
'Car',
'Bal',
$elProperty->getAttribute('Text')
)
);
}

PHP Query XML with xPath

I have an XML Structure like the following:
<Tickets>
<EventsPoints>
<Event ID="23">
<PerformanceName>U2</PerformanceName>
<EventDate>25/05/2012</EventDate>
<EventPrice>75.00</EventPrice>
</Event>
<Event ID="27">
<PerformanceName>Jedward</PerformanceName>
<EventDate>28/05/2012</EventDate>
<EventPrice>20.00</EventPrice>
</Event>
<Event ID="27">
<PerformanceName>Rolling Stones</PerformanceName>
<EventDate>03/12/2012</EventDate>
<EventPrice>80.00</EventPrice>
</Event>
</EventsPoints>
</Tickets>
Basically I want to search this XML for a certain performance name, say "U2", and then return that entire XML block (i.e. that performance name, event date and price - all in formatted XML and saved in a separate xml file)
This is my php code but it doesn't seem to be extracting the data correctly:
$srcDom = new DOMDocument;
$srcDom->load('/var/www/html/xml/searchfile.xml');
$xPath = new DOMXPath($srcDom);
foreach ($srcDom->getElementsByTagName('Event') as $event) {
$dstDom = new DOMDocument('1.0', 'utf-8');
$dstDom->appendChild($dstDom->createElement('EventsPricePoints'));
$dstDom->documentElement->appendChild($dstDom->importNode($event, true));
$allEventsForVenue = $xPath->query(
sprintf(
'/Tickets/EventsPoints/Event/PerformanceName[.="U2"]'
)
);
foreach ($allEventsForVenue as $event) {
$dstDom->documentElement->appendChild($dstDom->importNode($event, true));
}
$dstDom->formatOutput = true;
$dstDom->save(sprintf('/var/www/html/xml/searchresults1.xml'));
}
Your XPath gets the PerformanceName element when you want the parent element. Change it to
/Tickets/EventsPoints/Event/PerformanceName[.="U2"]/..
or
/Tickets/EventsPoints/Event[PerformanceName[.="U2"]]
or import
$event->parentNode
Also, you dont need the first foreach. Remove it and move the code writing the $dstDom into the code iterating the XPath result. See http://codepad.org/zfOZXycZ
Also see:
DOMDocument in php and
http://schlitt.info/opensource/blog/0704_xpath.html

Using DOMXml and Xpath, to update XML entries

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.

Categories