xpath Error while searching attributes - php

Hello I would like to extract with xpath all items with the name="Frequency" which contains a certain value but it doesn't give me anything. What Am I doing wrong?
XML File:
<products>
<product>
<title>PT2400</title>
<ElectricSpecifications>
<item name="Frequency">2310 - 2485 MHz (WLAN, BLUETOOTH, ZIGBEE</item>
</ElectricSpecifications>
</product>
</products>
foreach ($xpath->query("//product/ElectricSpecifications[#item='Frequency'][contains(., ' $value')]") as $item) {
var_dump($item);
}

item is not an attribute of ElectricSpecifications, but its child node, so instead of
[#item='Frequency']
syntax, try
item[#name='Frequency']
And so complete XPath should be like
//product/ElectricSpecifications/item[#name='Frequency' and contains(., ' $value')]

Related

read xml content and add some numbers with php

I have an xml file but through php I would like to make some changes only for some products (each item has its own id.
Let me explain better on some products I would like to add the shipping cost with the price and at the item use grid put from 1 to 0.
<Products>
<Product>
<sku>35</sku>
<sku_manufacturer>test sku</sku_manufacturer>
<manufacturer>test manufacturer</manufacturer>
<ean>800000000000</ean>
<title><![CDATA[title test]]></title>
<description><![CDATA[description</description>
<product_price_vat_inc>8.08</product_price_vat_inc>
<shipping_price_vat_inc>4.99</shipping_price_vat_inc>
<quantity>2842</quantity>
<brand><![CDATA[Finder]]></brand>
<merchant_category><![CDATA[Home/test category]]></merchant_category>
<product_url><![CDATA[https://www.example.com]]></product_url>
<image_1><![CDATA[https://www.example.com]]></image_1>
<image_2><![CDATA[]]></image_2>
<image_3><![CDATA[]]></image_3>
<image_4><![CDATA[]]></image_4>
<image_5><![CDATA[]]></image_5>
<retail_price_vat_inc/>
<product_vat_rate>22</product_vat_rate>
<shipping_vat_rate>22</shipping_vat_rate>
<manufacturer_pdf/>
<ParentSKU/>
<parent_title/>
<Cross_Sell_Sku/>
<ManufacturerWarrantyTime/>
<use_grid>1</use_grid>
<carrier>DHL</carrier>
<shipping_time>2#3</shipping_time>
<carrier_grid_1>DHL</carrier_grid_1>
<shipping_time_carrier_grid_1>2#3</shipping_time_carrier_grid_1>
<carrier_grid_2/>
<shipping_time_carrier_grid_2/>
<carrier_grid_3/>
<shipping_time_carrier_grid_3/>
<carrier_grid_4/>
<shipping_time_carrier_grid_4/>
<carrier_grid_5/>
<shipping_time_carrier_grid_5/>
<DisplayWeight>0.050000</DisplayWeight>
<free_return/>
<min_quantity>1</min_quantity>
<increment>1</increment>
<sales>0</sales>
<eco_participation>0</eco_participation>
<shipping_price_supplement_vat_inc>0</shipping_price_supplement_vat_inc>
<Unit_count>-1.000000</Unit_count>
<Unit_count_type/>
</Product>
</Products>
You XML is a little large so let's strip it down for the example:
$xmlString = <<<'XML'
<Products>
<Product>
<sku>35</sku>
<title><![CDATA[title test]]></title>
<use_grid>1</use_grid>
</Product>
<Product>
<sku>42</sku>
<title><![CDATA[title test two]]></title>
<use_grid>1</use_grid>
</Product>
</Products>
XML;
DOM is a standard API for XML manipulation. PHP supports it and Xpath expressions for fetching nodes.
$document = new DOMDocument('1.0', "UTF-8");
// $document->load($xmlFile);
$document->loadXML($xmlString);
// $xpath for fetching node using expressions
$xpath = new DOMXpath($document);
// iterate "Product" nodes with a specific "sku" child
foreach ($xpath->evaluate('//Product[sku="35"]') as $product) {
// output sku and title for validation
var_dump(
$xpath->evaluate('string(sku)', $product),
$xpath->evaluate('string(title)', $product)
);
// iterate the "use_grid" child elements
foreach ($xpath->evaluate('./use_grid', $product) as $useGrid) {
// output current value
var_dump(
$useGrid->textContent
);
// change it
$useGrid->textContent = "0";
}
}
echo "\n\n", $document->saveXML();
Output:
string(2) "35"
string(10) "title test"
string(1) "1"
<?xml version="1.0"?>
<Products>
<Product>
<sku>35</sku>
<title><![CDATA[title test]]></title>
<use_grid>0</use_grid>
</Product>
<Product>
<sku>42</sku>
<title><![CDATA[title test two]]></title>
<use_grid>1</use_grid>
</Product>
</Products>
Xpath::evaluate()
Xpath::evaluate() fetches nodes using an Xpath expression. The result type depends on the expression. A location path like //Product[sku="35"] will return a list of nodes (DOMNodeList). However Xpath functions inside the can return a scalar value - string(sku) will return the text content of the first sku child node as a string or an empty string.
DOMNode::$textContent
Reading $node->textContent will return all the text inside a node - including inside descendant elements.
Writing it replaces the content while taking care of the escaping.

Merge two variable SimpleXML Objects in PHP

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.

How should I modify this for each statement to only display entries where all of the values are unique?

foreach($resultXML->products->children() as $product) {
echo "<p>".$product->{'advertiser-name'}." - ".$product->price."</p>
<p>".$product->{'description'}."</p>";
}
Suppose I wanted to screen out the ones that had the same title, and only display the first title that appears in the return results.
I'm not working with my own database, this is all about what's displayed.
I suppose the easiest way would be to keep track of the titles in an array, and checking it each iteration.
$titles = array();
foreach($resultXML->products->children() as $product) {
if (in_array($product->title, $titles) continue;
$titles[] = $product->title;
echo "<p>".$product->{'advertiser-name'}." - ".$product->price."</p>
<p>".$product->{'description'}."</p>";
}
Assuming that the title is contained in $product->title. You could do something fancier through array functions, but I don't see a reason to make a simple problem complicated.
You have not provided any exemplary XML, so given for
<?xml version="1.0" encoding="UTF-8"?>
<example>
<products>
<product>
<title>First Product</title>
<advertiser-name>First Name</advertiser-name>
</product>
</products>
<products>
<product>
<title>Second Product</title>
<advertiser-name>First Name</advertiser-name>
</product>
</products>
<products>
<product>
<title>Third Product</title>
<advertiser-name>Second Name</advertiser-name>
</product>
</products>
</example>
You want to get all product elements with an advertiser-name that is not an advertiser-name of all preceding product elements.
So for the XML above, that would be the 1st and 3rd product element.
You can write that down as an XPath expression:
/*/products/product[not(advertiser-name = preceding::product/advertiser-name)]
And as PHP code:
$xml = simplexml_load_string($buffer);
$expr = '/*/products/product[not(advertiser-name = preceding::product/advertiser-name)]';
foreach ($xml->xpath($expr) as $product) {
echo $product->asXML(), "\n";
}
This produces the following output:
<product>
<title>First Product</title>
<advertiser-name>First Name</advertiser-name>
</product>
<product>
<title>Third Product</title>
<advertiser-name>Second Name</advertiser-name>
</product>
So one answer to your question therefore is: Only query those elements from the document you're interested in. XPath can be used for that with SimpleXMLElement.
Related questions:
Implementing condition in XPath

How to output all XML element attributes using a foreach loop?

New to XML here.
Consider this simple XML schema:
<products>
<category name="furniture">
<item name="chair" name2="table">
<size>
<small>10</small>
<large>20</large>
</size>
</item>
<item name="cabinet">
<size>
<small>15</small>
<large>30</large>
</size>
</item>
<item name="shelf" name2="box" name3="frame">
<size>
<small>5</small>
<large>10</large>
</size>
</item>
</category>
</products>
Notice each <item> element has a different amount of attributes.
I've been trying to echo out the attributes using XPATH and a foreach loop without success.
Surely I'm missing a small piece of syntax.
$dom=simplexml_load_file("file.xml");
foreach ($dom->xpath("/products/category[#name='furniture']/item") as $parse)
echo'<tr><td>'.$parse->attributes().'</td></tr>';
$parse->attributes(); only gives me the first attribute of the element.
Output looks like this:
<tr><td>chair</td></tr>
<tr><td>cabinet</td></tr>
<tr><td>shelf</td></tr>
What am I missing?
Use a nested foreach loop:
foreach ($dom->xpath("/products/category[#name='furniture']/item") as $parse) {
foreach ($parse->attributes() as $attr) {
echo '<tr><td>'. $attr . '</td></tr>'."\n";
}
}

php only appending 3 out of 5 child nodes

This code is only appending 3 of the 5 name nodes. Why is that?
Here is the original XML:
It has 5 name nodes.
<?xml version='1.0'?>
<products>
<product>
<itemId>531670</itemId>
<modelNumber>METRA ELECTRONICS/MOBILE AUDIO</modelNumber>
<categoryPath>
<category><name>Buy</name></category>
<category><name>Car, Marine & GPS</name></category>
<category><name>Car Installation Parts</name></category>
<category><name>Deck Installation Parts</name></category>
<category><name>Antennas & Adapters</name></category>
</categoryPath>
</product>
</products>
Then is run this PHP code. which is suppossed to appened ALL name nodes into the product node.
<?php
// load up your XML
$xml = new DOMDocument;
$xml->load('book.xml');
// Find all elements you want to replace. Since your data is really simple,
// you can do this without much ado. Otherwise you could read up on XPath.
// See http://www.php.net/manual/en/class.domxpath.php
//$elements = $xml->getElementsByTagName('category');
// WARNING: $elements is a "live" list -- it's going to reflect the structure
// of the document even as we are modifying it! For this reason, it's
// important to write the loop in a way that makes it work correctly in the
// presence of such "live updates".
foreach ($xml->getElementsByTagName('product') as $product ) {
foreach($product->getElementsByTagName('name') as $name ) {
$product->appendChild($name );
}
$product->removeChild($xml->getElementsByTagName('categoryPath')->item(0));
}
// final result:
$result = $xml->saveXML();
echo $result;
?>
The end result is this and it only appends 3 of the name nodes:
<?xml version="1.0"?>
<products>
<product>
<itemId>531670</itemId>
<modelNumber>METRA ELECTRONICS/MOBILE AUDIO</modelNumber>
<name>Buy</name>
<name>Antennas & Adapters</name>
<name>Car Installation Parts</name>
</product>
</products>
Why is it only appending 3 of the name nodes?
You can temporarily add the name elements to an array before appending them, owing to the fact that you're modifying the DOM in real time. The node list generated by getElementsByTagName() may change as you are moving nodes around (and indeed that appears to be what's happening).
<?php
// load up your XML
$xml = new DOMDocument;
$xml->load('book.xml');
// Array to store them
$append = array();
foreach ($xml->getElementsByTagName('product') as $product ) {
foreach($product->getElementsByTagName('name') as $name ) {
// Stick $name onto the array
$append[] = $name;
}
// Now append all of them to product
foreach ($append as $a) {
$product->appendChild($a);
}
$product->removeChild($xml->getElementsByTagName('categoryPath')->item(0));
}
// final result:
$result = $xml->saveXML();
echo $result;
?>
Output, with all values appended:
<?xml version="1.0"?>
<products>
<product>
<ItemId>531670</ItemId>
<modelNumber>METRA ELECTRONICS/MOBILE AUDIO</modelNumber>
<name>Buy</name><name>Car, Marine & GPS</name><name>Car Installation Parts</name><name>Deck Installation Parts</name><name>Antennas & Adapters</name></product>
</products>
You're modifying the DOM tree as you're pulling results from it. Any modifications to the tree that cover the results of a previous query operation (your getElementsByTagName) invalidate those results, so you're getting undefined results. This is especially true of operations that add/remove nodes.
You're moving nodes as you're iterating through them so 2 are being skipped. I'm not a php guy so I can't give you the code to do this, but what you need to do is build a collection of the name nodes and iterate through that collection in reverse.
A less complicated way to do it is to manipulate the nodes with insertBefore
foreach($xml->getElementsByTagName('name') as $node){
$gp = $node->parentNode->parentNode;
$ggp = $gp->parentNode;
// move the node above gp without removing gp or parent
$ggp->insertBefore($node,$gp);
}
// remove the empty categoryPath node
$ggp->removeChild($gp);

Categories