I have two xml files with a similar structure, and I need to merge them based on a common attribute. To be more explicit, here are two samples:
file1.xml
<Products>
<record ProductId="366" ProductName="Test" ProductCategory="Categ1"></record>
</Products>
file2.xml
<Productprices>
<record ProductId="366" ProductPrice="10" ProductVAT="24"></record>
</Productprices>
The common attribute in both files is ProductId. I need to merge all the attributes so that the combined file would look like this:
<Products>
<record ProductId="366" ProductName="Test" ProductCategory="Categ1" ProductPrice="10" ProductVAT="24"></record>
</Products>
Unfortunately, all I have managed to do so far is simply merge the two files, the merged file looks like this:
<Products>
<record ProductId="366" ProductName="Test" ProductCategory="Categ1"></record>
<record ProductId="366" ProductPrice="10" ProductVAT="24"></record>
</Products>
This is the PHP code I used:
$doc1 = new DOMDocument();
$doc1->load('file1.xml');
$doc2 = new DOMDocument();
$doc2->load('file2.xml');
$res1 = $doc1->getElementsByTagName('Products')->item(0);
$items2 = $doc2->getElementsByTagName('record');
for ($i = 0; $i < $items2->length; $i ++) {
$item2 = $items2->item($i);
$item1 = $doc1->importNode($item2, true);
$res1->appendChild($item1);
}
$doc1->save('file1.xml');
Is there any way I can merge all the attributes into one record based on the common ProductId by using DomDocument? I would rather not go into XSLT.
Any help will be greatly appreciated.
Thanks in advance.
I use Xpath to fetch nodes and values from DOM. In your case I see two tasks.
One task to iterate all record elements in on document, fetch the attributes of the matching element from the second document and copy the attributes.
The other task to iterate all record elements in the second document and add them to the first if here is no element with that ProductId.
$xmlOne = <<<'XML'
<Products>
<record ProductId="366" ProductName="Test" ProductCategory="Categ1"></record>
</Products>
XML;
$xmlTwo = <<<'XML'
<Productprices>
<record ProductId="366" ProductPrice="10" ProductVAT="24"></record>
<record ProductId="444" ProductPrice="23" ProductVAT="32"></record>
</Productprices>
XML;
$targetDom = new DOMDocument();
$targetDom->loadXml($xmlOne);
$targetXpath = new DOMXpath($targetDom);
$addDom = new DOMDocument();
$addDom->loadXml($xmlTwo);
$addXpath = new DOMXpath($addDom);
// merge attributes of record elements depending on ProductId
foreach ($targetXpath->evaluate('//record[#ProductId]') as $record) {
$productId = $record->getAttribute('ProductId');
foreach ($addXpath->evaluate('//record[#ProductId='.$productId.']/#*') as $attribute) {
if (!$record->hasAttribute($attribute->name)) {
$record->setAttribute($attribute->name, $attribute->value);
}
}
}
// copy records elements that are not in target dom
foreach ($addXpath->evaluate('//record[#ProductId]') as $record) {
$productId = $record->getAttribute('ProductId');
if ($targetXpath->evaluate('count(//record[#ProductId='.$productId.'])') == 0) {
$targetDom->documentElement->appendChild(
$targetDom->importNode($record)
);
}
}
echo $targetDom->saveXml();
You can use the attribute() function of SimpleXML
$xml = simplexml_load_file($filename);
foreach($xml->Products->record->attributes() as $attribute => $value) {
//do something
}
Related
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>
So i have an Xml file like
<cars>
<id>1</id>
<photos>
<img>http://sit.com/img.jpg</img>
<img>http://sit.com/img.jpg</img>
<img>http://sit.com/img.jpg</img>
<img>http://sit.com/img.jpg</img>
</photos>
</cars>
So i need to change all tag name to alternative and i need get something like
<cars>
<ex_id>1</ex_id>
<images>
<photo>http://sit.com/img.jpg</photo>
<photo>http://sit.com/img.jpg</photo>
<photo>http://sit.com/img.jpg</photo>
<photo>http://sit.com/img.jpg</photo>
</images>
</cars>
My code is
foreach ($dom->getElementsByTagName('cars') as $item) {
for ($i = 0; $i < $item->childNodes->length; ++$i) {
$car = $item->childNodes->item($i);
$NewElement = $dom->createElement($newName,$value);
$car->parentNode->replaceChild($NewElement->cloneNode(TRUE), $car);
}
}
Do something like that
<cars>
<ex_id>1</ex_id>
<images/>
</cars>
So it cut all childrens of <photos>, so my question is how to preserve children and also change childrens tags from <img> to <photo>
Here are several issues:
getElementByTagName() and $childNodes return 'live' lists, they change if you change the DOM. You can use iterator_to_array() to copy them into an array.
Here are not only element nodes. Comments, cdata sections and text (even containing only whitespaces) are nodes, too. If you iterate $childNodes you will have to validate the DOMNode::$nodeType.
Do not use the second argument of DOMDocument::createElement(). It has a broken escaping. Create a text node and append it.
1 and 2 go away if you use Xpath to fetch the nodes.
$dom = new DOMDocument();
$dom->loadXml($xml);
$xpath = new DOMXPath($dom);
foreach ($xpath->evaluate('/cars/images/img') as $photo) {
$newNode = $dom->createElement('photo');
$newNode->appendChild($dom->createTextNode($photo->textContent));
$photo->parentNode->replaceChild($newNode, $photo);
}
echo $dom->saveXml();
Output:
<?xml version="1.0"?>
<cars>
<ex_id>1</ex_id>
<images>
<photo>http://sit.com/img.jpg</photo>
<photo>http://sit.com/img.jpg</photo>
<photo>http://sit.com/img.jpg</photo>
<photo>http://sit.com/img.jpg</photo>
</images>
</cars>
Changing an DOM document is often a bad idea. It is easier to extract data from a source document and build a new target document:
$source = new DOMDocument();
$source->loadXml($xml);
$xpath = new DOMXPath($source);
$target = new DOMDocument();
$target->formatOutput = TRUE;
$cars = $target->appendChild($target->createElement('cars'));
$cars
->appendChild($target->createElement('ex_id'))
->appendChild(
$target->createTextNode(
$xpath->evaluate('string(/cars/id)')
)
);
$images = $cars->appendChild($target->createElement('images'));
foreach ($xpath->evaluate('/cars/photos/img') as $photo) {
$images
->appendChild($target->createElement('photo'))
->appendChild($target->createTextNode($photo->textContent));
}
echo $target->saveXml();
Output:
<?xml version="1.0"?>
<cars>
<ex_id>1</ex_id>
<images>
<photo>http://sit.com/img.jpg</photo>
<photo>http://sit.com/img.jpg</photo>
<photo>http://sit.com/img.jpg</photo>
<photo>http://sit.com/img.jpg</photo>
</images>
</cars>
Here is a language dedicated to transforming XML - XSLT. XSLT is supported by PHPs ext/xsl.
I need some assistance with the following issue. I have an xml file with the following structure:
<attributes>
<record ProductId="103" Compatibility="iPhone"/>
<record ProductId="103" Color="Black"/>
<record ProductId="103" Material="Leather"/>
<record ProductId="103" Collection="iPhone"/>
</attributes>
The problem is that I want to generate only one record per ProductId because it makes it easier for me to handle the information. The record should look like:
<record ProductId="103" Compatibility="iPhone" Color="Black" Material="Leather" Collection="iPhone"/>
I believe that using xpath is the way to go, but I just can't get the synthax right. I'm using a php script to parse the xml file.
Any assistance will be greatly appreciated.
Alternative you could just create a new one, but of course, get the necessary attributes first, gather them then apply them to the new one. Like this sample:
$xml_string = '<attributes><record ProductId="103" Compatibility="iPhone"/><record ProductId="103" Color="Black"/><record ProductId="103" Material="Leather"/><record ProductId="103" Collection="iPhone"/></attributes>';
$xml = simplexml_load_string($xml_string);
$record = new SimpleXMLElement('<attributes><record /></attributes>'); // create empty
$data = array();
foreach($xml->record as $entry) {
$attributes = $entry->attributes(); // get attributes
foreach($attributes as $key => $attribute) {
$data[$key] = (string) $attribute; // put it inside a container temporarily
}
}
// add the gathered attributes
foreach($data as $key => $value) {
$record->record->addAttribute($key, $value);
}
echo $record->asXML();
// echo htmlentities($record->asXML());
// <?xml version="1.0"?> <attributes><record ProductId="103" Compatibility="iPhone" Color="Black" Material="Leather" Collection="iPhone"/></attributes>
Trying to get all URLs values from xml.
I have hundreds of entry exactly in the form like e.g. this entry 16:
<?xml version="1.0" encoding="utf-8" ?>
<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<entries>
<entry id="16">
<revision number="1" status="accepted" wordclass="v" nounclasses="" unverified="false"></revision>
<media type="audio" url="http://website.com/file/65.mp3" />
</entry>
<entry id="17">
....
</entry>
</entries>
</root>
I am using this code but cannot get it to work. Why?
$doc = new DOMDocument;
$doc->Load('data.xml');
$xpath = new DOMXPath($doc);
$query = '//root/entries/entry/media';
$entries = $xpath->query($query);
What is the correc query for that? Best would be to only get the url value.
Your query probably returns the proper elements, but by default gives you the content of the media tag ( which in your case are empty, since the tag is self-closing ).
To get the url attribute of the tag you should use getAttribute(), example :
$entries = $xpath->query('//root/entries/entry/media');
foreach($entries as $entry) {
print $entry->getAttribute("url")."<br/>";
}
Or you should just xpath-query the attribute instead and read out it's value:
$urlAttributes = $xpath->query('//root/entries/entry/media/#url');
#####
foreach ($urlAttributes as $urlAttribute)
{
echo $urlAttribute->value, "<br/>\n";
#####
}
See DOMAttr::$valueDocs:
value
The value of the attribute
I would do that with SimpleXML actually:
$file = 'data.xml';
$xpath = '//root/entries/entry/media/#url';
$xml = simplexml_load_file($file);
$urls = array();
if ($xml) {
$urls = array_map('strval', $xml->xpath($xpath));
}
Which will give you all URLs as strings inside the $urls array. If there was an error loading the XML file, the array is empty.
I am have two xml files.. I first get one and loop through it then I need to take an id from the first xml file and find it in the second one and echo out the results associated with that id. If I were to do this with SQL I would simply do this:
$query = (SELECT * FROM HotelSummary WHERE roomTypeCode = '$id') or die();
while($row=mysql_fetch_array($query)){
$name = $row['Name'];
}
echo $name;
How can I do this is in xml and php??
I recommend you to read the DOMDocument documentation.
It's quite heavy but also powerful (not always clear what happens, but the Internet shold always give you a solution)
You can simply walk through your first document, finding your Id and then find your DOMElement via an XPath.
<?php
$dom = new DOMDocument();
$dom->load('1.xml');
foreach ($dom->getElementsByTagName('article') as $node) {
// your conditions to find out the id
$id = $node->getAttribute('id');
}
$dom = new DOMDocument();
$dom->load('2.xml');
$xpath = new DOMXPath($dom);
$element = $xpath->query("//*[#id='".$id."']")->item(0);
// would echo "top_2" based on my example files
echo $element->getAttribute('name');
Based on following test files:
1.xml
<?xml version="1.0" encoding="UTF-8"?>
<articles>
<article id="foo_1">
<title>abc</title>
</article>
<article id="foo_2">
<title>def</title>
</article>
</articles>
2.xml
<?xml version="1.0" encoding="UTF-8"?>
<tests>
<test id="foo_1" name="top_1">
</test>
<test id="foo_2" name="top_2">
</test>
</tests>
Use SimpleXML to create an object representation of the file. You can then loop through the elements of the Simple XML object.
Depending on the format of the XML file:
Assuming it is:
<xml>
<roomTypeCode>
<stuff>stuff</stuff>
<name>Skunkman</name>
</roomTypeCode>
<roomTypeCode>
<stuff>other stuff</stuff>
<name>Someone Else</name>
</roomTypeCode>
</xml>
It would be something like this:
$xml = simplexml_load_file('xmlfile.xml');
for($i = 0; $i < count($xml->roomTypeCode); $i++)
{
if($xml->roomTypeCode[$i]->stuff == "stuff")
{
$name = $xml->roomTypeCode[$i]->name;
}
}
That connects to the XML file, finds how many roomTypeCode entries there are, searches for the value of "stuff" within and when it matches it correctly, you can access anything having to do with that XML entry.