I (conceptually) understand which steps i have to take, but i can't translate it to a working code.
I have a XML feed with a structure like this:
<item id="1">
<properties>
<property name="region">
<value>Cote d'azur</value>
</property>
</properties>
</item>
<item id="2">
<properties>
<property name="region">
<value>Côte d'Azur</value>
</property>
</properties>
</item>
What i need is the feed to use consequent names, so i have to loop through each property with the name attribute and replace the value, but how?
So far i'm here, but this doesn't work
$xml_src = 'feed.xml';
$document = new DOMDocument();
$document->load($xml_src);
$xpath = new DOMXpath($document);
$regions = $xpath->evaluate('//property[#name = "region"]');
foreach($regions as $region){
$newregion = $document->createElement('value', str_replace("Cote d'azur","Côte d'Azur",$region->nodeValue));
$region->parentNode->replaceChild($newregion, $region);
}
echo $document->saveXml();
I get this error:
Warning: DOMDocument::createElement(): unterminated entity reference Ylläs in .. on line 17
Line 17:
$newregion = $document->createElement('value', str_replace("Cote d'azur","Côte d'Azur",$region->nodeValue));
To make it even more complicated, i sometimes have three value elements in each property with the name city. In that case i need to select the third element.
I hope anybody can help me out
The error message is from a bug in PHP. Do not use the second argument for DOMDocument::createElement(). Create and append a text node to make sure that special characters are escaped into entities.
https://stackoverflow.com/a/27225157/2265374
Anything in a DOM is a node. Not only the element, but attribute and texts, too. You can work on the text nodes inside the value elements directly:
$document = new DOMDocument();
$document->loadXml($xml);
$xpath = new DOMXpath($document);
foreach ($xpath->evaluate('//property[#name = "region"]/value/text()') as $text) {
$text->data = str_replace("Cote d'azur","Côte d'Azur", $text->data);
}
echo $document->saveXml();
Simple:
$regions = $xpath->query('//property[#name = "region"]/value');
foreach($regions as $region){
$region->nodeValue = str_replace("Cote d'azur","Côte d'Azur",$region->nodeValue);
}
echo $document->saveXml();
Related
Let's say I have the following .xml file:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<item>
<name>Foo</name>
</item>
<item>
<name>Bar</name>
</item>
</root>
In this sample file, I'm trying to append new nodes <item> to node <root> after the last node <item>.
I'm trying to append newly created <item> nodes after the last <item> node in the <root> node in the .xml file.
<?php
$file = new DOMDocument;
$file->load("xml.xml");
$file->loadXML($file->saveXML());
$root = $file->getElementsByTagName('root')->item(0);
foreach (["Foo_1", "Bar_2", "Foo_3", "Bar_4"] as $val) {
$item = new DOMElement('item');
$item->appendChild(new DOMElement('name', $val));
$root->appendChild(item);
}
?>
But I'm getting an error:
Fatal error: Uncaught Error: Call to a member function appendChild() on null in C:\Users\pfort\Desktop\p.php:12
Stack trace:
#0 {main}
thrown in C:\Users\user_acer\Desktop\p.php on line 12
What am I doing wrong?
There's multiple issues with your example code. I will address the error you received first:
There is no element <terminy> in your example XML, so
$root = $file->getElementsByTagName('terminy')->item(0);
will return null. That's why you are receiving the
Call to a member function appendChild() on null
error at
$root->appendChild(item);
Also, item is a typo, because it's not a valid variable name (but a name for a non-existent constant); you meant $item.
I'm assuming "terminy" means something similar to "root" in your native language and that you actually meant to write
$root = $file->getElementsByTagName('root')->item(0);
By the way: if you want a reference to the root node of an XML document, you can also use $file->docomentElement.
However, there are other issues with your example code:
$file->load("xml.xml");
$file->loadXML($file->saveXML()); // why are you reloading it in this way?
The last line is unnecessary. You are reloading the same XML again. Is it for formatting purposes? If so, there's a better option available:
$file->preserveWhiteSpace = false;
$file->formatOutput = true;
$file->load("xml.xml");
Lastly: you cannot append children to a node that has not been associated with a document yet. So, to create a new item and associate it with the document, you either do (recommended):
// automatically associate new nodes with document
$item = $file->createElement('item');
$item->appendChild($file->createElement('name', $val));
or (more cumbersome):
// import nodes to associate them with document
$item = $file->importNode(new DOMElement('item'));
$item->appendChild($file->importNode(new DOMElement('name', $val)));
So, putting it all together it becomes:
<?php
$xml = <<<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<root>
<item>
<name>Foo</name>
</item>
<item>
<name>Bar</name>
</item>
</root>
XML;
$file = new DOMDocument;
$file->preserveWhiteSpace = false;
$file->formatOutput = true;
$file->loadXML($xml); // (for demo purpose loading above XML) replace this with $file->load("xml.xml"); in your actual code
$root = $file->documentElement;
foreach (["Foo_1", "Bar_2", "Foo_3", "Bar_4"] as $val) {
$item = $file->createElement('item');
$item->appendChild($file->createElement('name', $val));
$root->appendChild($item);
}
echo $file->saveXML();
**PROBLEM SOLVED**
I lost too much time on this problem. The good news is, I already know how to get what I need. Here I offer a solution - for everyone who will need to solve the same problem.
Perhaps this solution will be useful for anyone who needs it.
<?php
// snippet of xml temple
$xml = <<<XML
<item date="%s" status="%s">
<name>%s</name>
</item>
XML;
// prepare snippet
$xmlSnippet = sprintf($xml, "2022-11-21", 0, "Foo Bar");
// new DOMDocument
$dom = new DOMDocument;
$dom->preserveWhiteSpace = 0;
$dom->formatOutput = 1;
// load of .xml file content and load to DOMDocument object
$file = simplexml_load_file("xml.xml");
$dom->loadXML($file->asXML());
// creating of fragment from snippet
$fragment = $dom->createDocumentFragment();
$fragment->appendXML($xmlSnippet);
//append the snippet to the DOMDocument
// and save it to the xml.xml file
$dom->documentElement->appendChild($fragment);
$dom->save("xml.xml");
?>
Result:
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();
Hi I looped through an XML Document and searching for specific Word (that works) and after that i would like to a a certain Element if the word is included in that part.
<product>
<title>TestProduct</title>
<Specifications>
<item name="Specifications1">Test</item>
<item name="Specifications2">Hello World</item>
</Specifications>
<body>
<item name="Color">Black</item>
</body>
</product>
I tried this:
<?php
$dom = new DOMDocument;
$dom->load('Test.xml');
# set up the xpath
$xpath = new DOMXPath($dom);
$append= $dom->getElementsByTagName('Text');
print_r($append);
foreach ($xpath->query("*[contains(., 'Test')]") as $item) {
$element = $dom->createElement('ID', '123');
$append->appendChild($element);
}
?>
but it doesn't work can someone give me a hint? Tanks ;)
The problem is that your trying to add the new element into the wrong place. This combines the getElementsByTagName and the XPath into one loop and then adds the new element to any element it finds.
$dom = new DOMDocument;
$dom->load('Test.xml');
$xpath = new DOMXPath($dom);
foreach ($xpath->query("//*[contains(., 'Black')]") as $item) {
$element = $dom->createElement('ID', '123');
$item->appendChild($element);
}
echo $dom->saveXML();
See how the XPath finds any element which contains 'Test'. Then it adds the new element to each element it finds.
The main thing that I have changed is you were adding the ID to $append and not $item.
If you wanted to just update the first instance of a particular piece of text...
$matchList = $xpath->query("//*[contains(text(), 'Black')]");
$element = $dom->createElement('ID', '123');
$matchList->item(0)->appendChild($element);
echo $dom->saveXML().PHP_EOL;
I have a situation that's driving me crazy. I want to search my feed for an element with a certain value (deriving from an array) and if that value is found, change the value of it's previous sibling element.
My feed looks like this
<products>
<product>
<properties>
<property name="category">
<value>Fruits</value>
</property>
<property name="item">
<value>Banana</value>
</property>
</properties>
</product>
<product>
<properties>
<property name="category">
<value>Fruits</value>
</property>
<property name="item">
<value>Apple</value>
</property>
</properties>
</product>
<product>
<properties>
<property name="category">
<value>Fruits</value>
</property>
<property name="item">
<value>Carrot</value>
</property>
</properties>
</product>
</products>
As you can see, there can be some errors in the feed. For these instances i made an array with the appropriate value, like so:
$replacements = Array(
"Carrot" => "Vegetable"
);
Now i thought to select every property with the attribute item that has the value as in $replacements's key, then select the previous sibling element with the attribute category and change this value with the matching $replacements's value.
I came up with this, but that only gives me a white screen with no output at all
$xml_src = 'feed.xml';
$document = new DOMDocument();
$document->load($xml_src);
$xpath = new DOMXpath($document);
$query = '//property[#name = "item"]';
$entries = $xpath->query($query);
foreach ($entries as $entry) {
if(array_key_exists($entry->nodeValue,$replacements)){
$entry->previousSibling->previousSibling->nodeValue = $replacements[$entries->nodeValue];
}
}
But i don't understand why it doesn't output anything
There's a couple things a bit off here:
array_key_exists($entry->nodeValue,$replacements)
The nodeValue is going to contain all the text content within the property element and its descendants - including newlines and spaces.
... = $replacements[$entries->nodeValue];
You're likely looking for $replacements[$entry->nodeValue] but again you've got the same problem as above with the white space. Not only that but you'd be replacing the entirety of the prior property's nodeValue with text so:
<property name="category">
<value>Fruits</value>
</property>
would become just:
<property name="category">Vegetable</property>
To fix it all up I'd recommend adjusting the query, as well as how you're addressing the target value to replace in order to get rid of the ->previousSibling->previousSibling chaining and be a bit more explicit.
Example:
foreach ($xpath->query('//property[#name="item"]/value') as $node) {
if (array_key_exists(trim($node->textContent), $replacements)) {
$target = $xpath->query(
'preceding-sibling::property/value/text()',
$node->parentNode
)->item(0);
$target->parentNode->replaceChild(
$dom->createTextNode($replacements[trim($node->textContent)]),
$target
);
}
}
echo $dom->saveXML();
Output:
...
<product>
<properties>
<property name="category">
<value>Vegetable</value>
</property>
<property name="item">
<value>Carrot</value>
</property>
</properties>
</product>
</products>
You need to return Value and in order to do that you need to correct your query to this
$query = '//property[#name = "item"]/value';
You can try to use the complete path, so:
$query = '//products/product/properties/property[#name = "item"]/value';
$entries = $xpath->query($query);
foreach ($entries as $entry) {
if(array_key_exists($entry->nodeValue,$replacements)){
$entry->previousSibling->previousSibling->nodeValue = $replacements[$entries->nodeValue];
}
}
Also, you can try to use a context (Manual):
$context = $document->getElementsByTagName('products')->item(0);
$query = './/property[#name = "item"]/value';//add a '.' before the query to make it relative to the context
$entries = $xpath->query($query, $context);
foreach ($entries as $entry) {
if(array_key_exists($entry->nodeValue,$replacements)){
$entry->previousSibling->previousSibling->nodeValue = $replacements[$entries->nodeValue];
}
}
Both solutions work for me in very similar situations.
i have a XML document that looks like this:
<body>
<item id="9982a">
<value>ab</value>
</item>
<item id="9982b">
<value>abc</value>
</item>
etc...
</body>
Now, i need to get the value for a key, the document is very very big, is there any way to go directly to the key when i know the id? Rather then loop it?
Something like:
$xml = simplexml_load_string(file_get_contents('http://somesite.com/new.xml'));
$body = $xml->body;
$body->item['id'][9982a]; // ab
?
xpathis your friend, you can stay with simplexml:
$xml = simplexml_load_string($x); // assume XML in $x
$result = $xml->xpath("/body/item[#id = '9982a']/value")[0]; // requires PHP >= 5.4
echo $result;
Comment:
in PHP < 5.4, do...
$result = $xml->xpath("/body/item[#id = '9982a']/value");
$result = $result[0];
see it working: https://eval.in/101766
Yes, use Simple HTML DOM Parser instead of SimpleXML.
It would be as easy as:
$xml->find('item[id="9982b"]',0)->find('value',0)->innertext;
It is possible with DOMXpath::evaluate() to fetch scalar values from a DOM using xpath expressions:
$xml = <<<'XML'
<body>
<item id="9982a">
<value>ab</value>
</item>
<item id="9982b">
<value>abc</value>
</item>
</body>
XML;
$dom = new DOMDocument;
$dom->loadXml($xml);
$xpath = new DOMXpath($dom);
var_dump(
$xpath->evaluate('string(//body/item[#id="9982b"]/value)')
);