I've been trying to merge two XML files I use to build my menubar in my web application for hours, but I can't get it to work.
I have my main XML file which looks like this:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<root>
<version>1.0.0</version>
<menu>
<Category1>
<item>
<id>Cake</id>
<nr>1</nr>
<hint>I like these</hint>
<userlevel>5</userlevel>
</item>
<item>
<id>Cake 2</id>
<nr>2</nr>
<hint>I like these too, but only for me</hint>
<userlevel>10</userlevel>
</item>
<Category1>
<Category2WithApples>
<item>
<id>Apple Cake</id>
<nr>1</nr>
<hint>Sweet</hint>
<userlevel>5</userlevel>
</item>
<item>
<id>Rainbow Cake</id>
<nr>2</nr>
<hint>Mine!!</hint>
<userlevel>10</userlevel>
</item>
<Category2WithApples>
</menu>
</root>
Now, I want each user to be able to load in his custom XML which is in the same folder as the main.xml which looks like this:
<CategoryMyOwn>
<item>
<id>Item in my Category</id>
<nr>0</nr>
<hint>Some text</hint>
<userlevel>0</userlevel>
</item>
</CategoryMyOwn>
<Category1>
<item>
<id>Item in existing category</id>
<nr>0</nr>
<hint>Some text</hint>
<userlevel>0</userlevel>
</item>
</Category1>
I've tried solutions from
http://php.net/manual/de/ref.simplexml.php
php recursion, function doesn't return any value
http://durgagupta.com.np/php-how-to-merge-two-simplexml-objects/
but they all do not work at all for me or just append the second file to the end of my main.xml. So, my question is, how do I properly merge the user.xml into my main.xml so it looks like this:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<root>
<version>1.0.0</version>
<menu>
<Category1>
<item>
<id>Cake</id>
<nr>1</nr>
<hint>I like these</hint>
<userlevel>5</userlevel>
</item>
<item>
<id>Cake 2</id>
<nr>2</nr>
<hint>I like these too, but only for me</hint>
<userlevel>10</userlevel>
</item>
<item>
<id>Item in existing category</id>
<nr>0</nr>
<hint>Some text</hint>
<userlevel>0</userlevel>
</item>
<Category1>
<Category2WithApples>
<item>
<id>Apple Cake</id>
<nr>1</nr>
<hint>Sweet</hint>
<userlevel>5</userlevel>
</item>
<item>
<id>Rainbow Cake</id>
<nr>2</nr>
<hint>Mine!!</hint>
<userlevel>10</userlevel>
</item>
<Category2WithApples>
<CategoryMyOwn>
<item>
<id>Item in my Category</id>
<nr>0</nr>
<hint>Some text</hint>
<userlevel>0</userlevel>
</item>
</CategoryMyOwn>
</menu>
</root>
Your second XML is not a document, XML documents need to have a document element node. In other words here at the top level only a single element node is allowed. All other element nodes have to be descendants of that node.
You can treat this as an XML fragment however. A fragment is the inner XML of an element node.
In both cases it easier to use DOM for that.
Append a fragment to a parent element node
Let's keep it simple for the first step and append the fragment to the menu node.
$document = new DOMDocument();
$document->loadXml($targetXml);
$xpath = new DOMXpath($document);
$fragment = $document->createDocumentFragment();
$fragment->appendXml($fragmentXml);
foreach ($xpath->evaluate('/root/menu[1]') as $menu) {
$menu->appendChild($fragment);
}
echo $document->saveXml();
The Xpath expression can /root/menu[1] selects the first menu element node inside the root. This can be only one node or none.
A document fragment in DOM is a node object and can be appended like any other node (element, text, ...).
Merging nodes
Merging the category nodes is a little more difficult. But Xpath will help.
$document = new DOMDocument();
$document->loadXml($targetXml);
$xpath = new DOMXpath($document);
$fragment = $document->createDocumentFragment();
$fragment->appendXml($fragmentXml);
$menu = $xpath->evaluate("/root/menu[1]")->item(0);
foreach ($xpath->evaluate('*', $fragment) as $category) {
$targets = $xpath->evaluate("{$category->nodeName}[1]", $menu);
if ($targets->length > 0) {
$targetCategory = $targets->item(0);
foreach ($category->childNodes as $item) {
$targetCategory->appendChild($item);
}
} else {
$menu->appendChild($category);
}
}
echo $document->saveXml();
Fetching the menu node
$menu = $xpath->evaluate("/root/menu[1]")->item(0);
This is about the same like in the first simple example. It fetch the menu nodes in root and returns the first found node. You should check if the list contained a node. But for this example just take it for guaranteed.
Iterating the fragment
foreach ($xpath->evaluate('*', $fragment) as $category) {
...
}
* is a simple Xpath expression that returns any element child node. The fragment can contain other nodes (whitespace, text, comment, ...). The second argument for DOMXpath::evaluate() is the context for the Xpath expression.
Fetching the target category
Next you need to fetch the category node with the same name from the target document. This will return a list with one node or an empty list.
$targets = $xpath->evaluate("{$category->nodeName}[1]", $menu);
if ($targets->length > 0) {
...
} else {
...
}
Append to the found target category
If the category exists append all child nodes from the category in the fragment to the target.
$targetCategory = $targets->item(0);
foreach ($category->childNodes as $item) {
$targetCategory->appendChild($item);
}
Append a category
$menu->appendChild($category);
If the category doesn't exists, just append it to the menu.
Related
I am loading a RSS XML feed in PHP with DOMDocument. That works fine. I need to parse my XML, find specific values and then only display certain nodes again.
The XML looks like that...
<rss version="2.0">
<channel>
<title>Title</title>
<link></link>
<item>
<title>Title #1</title>
<description>Here I want to filter</description>
</item>
<item>
<title>Title #2</title>
<description>Should not be displayed</description>
</item>
</channel>
I want to search inside the description tag, and if a keyword has been found I want to display the item. If it is not found, I want to delete the parent item.
That is what I tried so far...
<?php
header('Content-Type: text/xml');
// Load our XML document
$rss = new DOMDocument();
$rss->load('https://myurl');
$description = $rss->getElementsByTagName('description');
foreach ($description as $node) {
$s = $node->nodeValue;
if (strpos($s, 'filter') !== false)
{
//found the keyword, nothing to delete
}
else
{
//didnt find it, now delete item
$node->parentNode->parentNode->removeChild($node->parentNode);
}
}
echo $description->saveXml();
I am trying to get all description nodes, check if they contain the string and if not, delete the parent. The searching for the String works, however deleting the node not. If I echo my XML, nothing has changed.
getElementsByTagName() will return a "live" result. It will change if you modify the document. You could use iterator_to_array() to make a stable copy.
Another option is to use Xpath expressions to fetch specific nodes.
$document = new DOMDocument();
$document->loadXML($xmlString);
$xpath = new DOMXpath($document);
// fetch items that contain "filter" in their description
$items = $xpath->evaluate('/rss/channel/item[contains(description, "filter")]');
foreach ($items as $item) {
// dump the title child element text content
var_dump($xpath->evaluate('string(title)', $item));
}
// fetch items that do not contain "filter" in their description
$items = $xpath->evaluate('/rss/channel/item[not(contains(description, "filter"))]');
foreach ($items as $item) {
// remove item element
$item->parentNode->removeChild($item);
}
echo $document->saveXML();
Output:
string(8) "Title #1"
<?xml version="1.0"?>
<rss version="2.0">
<channel>
<title>Title</title>
<link/>
<item>
<title>Title #1</title>
<description>Here I want to filter</description>
</item>
</channel>
</rss>
I load the following XML data into SimpleXML like this:
<?php
$xmlString = <<<'XML'
<?xml version="1.0"?>
<response>
<item key="0">
<title>AH 2308</title>
<field_a>3.00</field_a>
<field_b>7.00</field_b>
<field_d1>35.00</field_d1>
<field_d2>40.00</field_d2>
<field_e></field_e>
<field_g2></field_g2>
<field_g>M 45x1,5</field_g>
<field_gewicht>0.13</field_gewicht>
<field_gtin>4055953012781</field_gtin>
<field_l>40.00</field_l>
<field_t></field_t>
<field_abdrueckmutter>KM 9</field_abdrueckmutter>
<field_sicherung>MB 7</field_sicherung>
<field_wellenmutter>KM 7</field_wellenmutter>
</item>
<item key="1">
<title></title>
<field_a></field_a>
<field_b></field_b>
<field_d1></field_d1>
<field_d2></field_d2>
<field_e></field_e>
<field_g2></field_g2>
<field_g></field_g>
<field_gewicht></field_gewicht>
<field_gtin></field_gtin>
<field_l></field_l>
<field_t></field_t>
<field_abdrueckmutter></field_abdrueckmutter>
<field_sicherung></field_sicherung>
<field_wellenmutter></field_wellenmutter>
</item>
</response>
XML;
$xml = simplexml_load_string($xml);
How can I achieve the following result:
<?xml version="1.0"?>
<response>
<item key="0">
<title>AH 2308</title>
<field_a>3.00</field_a>
<field_b>7.00</field_b>
<field_d1>35.00</field_d1>
<field_d2>40.00</field_d2>
<field_e></field_e>
<field_g2></field_g2>
<field_g>M 45x1,5</field_g>
<field_gewicht>0.13</field_gewicht>
<field_gtin>4055953012781</field_gtin>
<field_l>40.00</field_l>
<field_t></field_t>
<field_abdrueckmutter>KM 9</field_abdrueckmutter>
<field_sicherung>MB 7</field_sicherung>
<field_wellenmutter>KM 7</field_wellenmutter>
</item>
<item key="1"></item>
</response>
To delete all empty elements, I could use the following working code:
foreach ($xml->xpath('/child::*//*[not(*) and not(text()[normalize-space()])]') as $emptyElement) {
unset($emptyElement[0]);
}
But that's not exactly what I want.
Basically, when the <title> element is empty, I want to remove it with all its siblings and keep the parent <item> element.
What's important: I also want to keep empty element, if the <title> is not empty. See <item key="0"> for example. The elements <field_e>, <field_g2> and <field_t>will be left untouched.
Is there an easy xpath query which can achieve that? Hope anyone can help. Thanks in advance!
This xpath query is working:
foreach ($xml->xpath('//title[not(text()[normalize-space()])]/following-sibling::*') as $emptyElement) {
unset($emptyElement[0]);
}
It keeps the <title> element but I can live with that.
DOM is more flexible manipulating nodes:
$document = new DOMDocument();
$document->loadXML($xmlString);
$xpath = new DOMXpath($document);
$expression = '/response/item[not(title[normalize-space()])]';
foreach ($xpath->evaluate($expression) as $emptyItem) {
// replace children with an empty text node
$emptyItem->textContent = '';
}
echo $document->saveXML();
I been searching information how to remove white spaces between tag values leaved by a PHP code when I export it to XML, I will explain detailed, first I load and XML then I do a search on the file with xPath, then I remove some elements that do not match some brands and finally I reexport it as a new XML, the problem is that this new XML is full of white spaces leaved by the code. I tried trim it but it doesn't seems to work correctly.
Here is my code:
<?php
$sXML = simplexml_load_file('file.xml'); //First load the XML
$brands = $sXML->xPath('//brand'); //I do a search for the <brand> tag
function filter(string $input) { //Then I give it a list of variables
switch ($input) {
case 'BRAND 3':
case 'BRAND 4':
return false;
default:
return true;
}
}
array_walk($brands, function($brand) { //I remove all elements do not match my list
$content = (string) $brand;
if (filter($content)) {
$item = $brand->xPath('..')[0];
unset($item[0]);
}
});
$sXML->asXML('filtred.xml'); // And finally export a new xml
?>
This one is the original XML:
<?xml version="1.0" encoding="utf-8"?>
<products>
<item>
<reference>00001</reference>
<other_string>PRODUCT 1</other_string>
<brand>BRAND 1</brand>
</item>
<item>
<reference>00002</reference>
<other_string>PRODUCT 2</other_string>
<brand>BRAND 2</brand>
</item>
<item>
<reference>00003</reference>
<other_string>PRODUCT 3</other_string>
<brand>BRAND 3</brand>
</item>
<item>
<reference>00004</reference>
<other_string>PRODUCT 4</other_string>
<brand>BRAND 4</brand>
</item>
<item>
<reference>00005</reference>
<other_string>PRODUCT 5</other_string>
<brand>BRAND 5</brand>
</item>
</products>
And the output of the script sends this:
<?xml version="1.0" encoding="utf-8"?>
<products>
<item>
<reference>00001</reference>
<other_string>PRODUCT 1</other_string>
<brand>BRAND 1</brand>
</item>
<item>
<reference>00002</reference>
<other_string>PRODUCT 2</other_string>
<brand>BRAND 2</brand>
</item>
<item>
<reference>00005</reference>
<other_string>PRODUCT 5</other_string>
<brand>BRAND 5</brand>
</item>
</products>
As you can see on the output, there is a white space between product 2 and product 5 and that I need to remove it. Any help will be appreciate.
You can force SimpleXML to trim all whitespace when it reads the file, by passing the LIBXML_NOBLANKS option to simplexml_load_file:
$sXML = simplexml_load_file('file.xml', null, LIBXML_NOBLANKS);
Then when you call ->asXML(), all the whitespace will be removed, and you'll get XML all on one line, like this:
<?xml version="1.0" encoding="utf-8"?>
<products><item><reference>00003</reference><other_string>PRODUCT 3</other_string><brand>BRAND 3</brand></item><item><reference>00004</reference><other_string>PRODUCT 4</other_string><brand>BRAND 4</brand></item></products>
To re-generate whitespace based on the remaining structure, you'll need to use DOM rather than SimpleXML - but that's easy to do without changing any of your existing code, because dom_import_simplexml simply "rewraps" the XML without reparsing it.
Then you can use the DOMDocument formatOutput property and save() method to "pretty-print" the document:
$sXML = simplexml_load_file('file.xml', null, LIBXML_NOBLANKS);
// ...
// process $sXML as before
// ...
$domDocument = dom_import_simplexml($sXML)->ownerDocument;
$domDocument->formatOutput = true;
echo $domDocument->save('filtered.xml');
Another possibility is to use preg_replace:
// Get simpleXml as string
$xmlAsString = $yourSimpleXmlObject->asXML();
// Remove newlines
$xmlAsString = preg_replace("/\n/", "", $xmlAsString);
// Remove spaces between tags
$xmlAsString = preg_replace("/>\s*</", "><", $xmlAsString);
var_dump($xmlAsString);
Now you get your XML as string in one line (including the XML declaration).
I have two XML files (that have a number of common nodes) that look a bit like these:
DESTINATION FILE: ('destination.xml')
<items>
<item>
<title>Item A</title>
<description>This is the description for Item A</description>
<id>1001</id>
</item>
<item>
<title>Item B</title>
<description>This is the description for Item B</description>
<id>1002</id>
</item>
<item>
<title>Item D</title>
<description>This is the description for Item D</description>
<id>1004</id>
</item>
and
SOURCE FILE: ('source.xml')
<items>
<item>
<title>Item A</title>
<description>This is the description for Item A</description>
<id>1001</id>
</item>
<item>
<title>Item C</title>
<description>This is the description for Item C</description>
<id>1003</id>
</item>
<item>
<title>Item B</title>
<description>This is the description for Item B</description>
<id>1002</id>
</item>
I need to grab the node from SOURCE with the 'id' matching '1003' (in this example) and import it into the DESTINATION. I'm looking for insight in using importNode (or a simpleXML option) and also the xpath in only getting only the node I need.
Just do that and it should work :
<?php
header('Content-type: application/xml'); //Just to test in the browser directly and have a good format
$docSource = new DOMDocument();
$docSource->loadXML(file_get_contents('source.xml'));
$docDest = new DOMDocument();
$docDest->loadXML(file_get_contents('destination.xml'));
$xpath = new DOMXPath($docSource);
$result = $xpath->query('//item[id=1003]')->item(0); //Get directly the node you want
$result = $docDest->importNode($result, true); //Copy the node to the other document
$items = $docDest->getElementsByTagName('items')->item(0);
$items->appendChild($result); //Add the copied node to the destination document
echo $docDest->saveXML();
To get the correct node, I think your XPath should look like this:
$xpath->query('/items/item/id[.="1003"]/..')
To import it to the other document, you'll need to create the document and call importNode with the second parameter set to true:
$newDom = new DOMDocument;
$newDom->load('destination.xml');
$newNode = $newDom->importNode($el, true);
$newDom->firstChild->appendChild($newNode);
file_put_contents('destination.xml', $newDom->saveXML());
I have an xml document with the following structure:
<?xml version="1.0" encoding="UTF-8"?>
<items>
<item>
<id>1</id>
<url>www.test.com</url>
</item>
<item>
<id>2</id>
<url>www.test2.com</url>
</item>
</items>
I would like to be able to search for a node value, such as the value of 1 for the id field. Then, once that node is found, select the parent node, which would be < item > and insert a new child within.
I know the concept of using dom document, but not sure how to do it in this instance.
This should be a start:
$dom = new DOMDocument;
$dom->loadXML($input);
$ids = $dom->getElementsByTagName('id');
foreach ($ids as $id) {
if ($id->nodeValue == '1') {
$child = $dom->createElement('tagname');
$child->appendChild($dom->createTextNode('some text'));
$id->parentNode->appendChild($child);
}
}
$xml = $dom->saveXML();
or something close to it.
You can do the same thing in a simpler way. Instead of looking for an <id/> node whose value is 1 then selecting its parent, you can reverse the relation and look for any node which has an <id/> child whose value is 1.
You can do that very easily in XPath, and here's how to do it in SimpleXML:
$items = simplexml_load_string(
'<?xml version="1.0" encoding="UTF-8"?>
<items>
<item>
<id>1</id>
<url>www.test.com</url>
</item>
<item>
<id>2</id>
<url>www.test2.com</url>
</item>
</items>'
);
$nodes = $items->xpath('*[id = "1"]');
$nodes[0]->addChild('new', 'value');
echo $items->asXML();