I have an xml document below:
<?xml version="1.0" encoding="UTF-8" ?>
<books>
<book>
<name>Title One</name>
<year>2014</year>
<authors>
<author>
<name>Author One</name>
</author>
</authors>
</book>
<book serie="yes">
<name>Title Two</name>
<year>2015</year>
<authors>
<author>
<name>Author two</name>
</author>
<author>
<name>Author three</name>
</author>
</authors>
</book>
<book serie="no">
<name>Title Three</name>
<year>2015</year>
<authors>
<author>
<name>Author four</name>
</author>
</authors>
</book>
</books>
I want to convert it into an array bellow.
array(
array('Tittle one', 2014, 'Author One'),
array('Tittle two', 2015, 'Author two, Author three'),
array('Tittle three', 2015, 'Author four'),
);
My code below is failing to produce the array structure that I would want:
function arrayRepresentation(){
$xmldoc = new DOMDocument();
$xmldoc->load("data/data.xml");
$parentArray = array();
foreach ($xmldoc->getElementsByTagName('book') as $item) {
$parentArray[] = array_generate($item);
}
var_dump($parentArray);
}
function array_generate($item){
$movieArray = array();
$childMovieArray = array();
for ($i = 0; $i < $item->childNodes->length; ++$i) {
$child = $item->childNodes->item($i);
if ($child->nodeType == XML_ELEMENT_NODE) {
if(hasChild($child)){
$childMovieArray = array_generate($child);
}
}
$movieArray[] = trim($child->nodeValue);
}
if(!empty($childMovieArray)){
$movieArray = array_merge($movieArray,$childMovieArray);
}
return $movieArray;
}
function hasChild($p)
{
if ($p->hasChildNodes()) {
foreach ($p->childNodes as $c) {
if ($c->nodeType == XML_ELEMENT_NODE)
return true;
}
}
}
arrayRepresentation();
Basically I am looping through the the nodes to get xml element values. I am then checking if I Node has more child nodes and if it has I loop through it again to get the values. I am failling to deduce a way that: (i) will not give me some empty array elements (ii) Check for any child nodes and put all the xml sibling elements in a single string
PHPs DOM supports Xpath expressions for fetching specific nodes and values. That drastically reduces the amount of loops and conditions you will need.
Here is a demo:
// bootstrap the XML document
$document = new DOMDocument();
$document->loadXML($xml);
$xpath = new DOMXpath($document);
$data = [];
// iterate the node element nodes
foreach ($xpath->evaluate('/books/book') as $book) {
$authors = array_map(
fn ($node) => $node->textContent,
// fetch author name nodes as an array
iterator_to_array($xpath->evaluate('authors/author/name', $book))
);
$data[] = [
// cast first name element child to string
$xpath->evaluate('string(name)', $book),
// cast first year element child to string
$xpath->evaluate('string(year)', $book),
implode(', ', $authors)
];
}
var_dump($data);
Related
I have an XML from Google with a content like this:
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">
<channel>
<title>E-commerce's products.</title>
<description><![CDATA[Clothing and accessories.]]></description>
<link>https://www.ourwebsite.com/</link>
<item>
<title><![CDATA[Product #1 title]]></title>
<g:brand><![CDATA[Product #1 brand]]></g:brand>
<g:mpn><![CDATA[5643785645]]></g:mpn>
<g:gender>Male</g:gender>
<g:age_group>Adult</g:age_group>
<g:size>Unica</g:size>
<g:condition>new</g:condition>
<g:id>fr_30763_06352</g:id>
<g:item_group_id>fr_30763</g:item_group_id>
<link><![CDATA[https://www.ourwebsite.com/product_1_url.htm?mid=62367]]></link>
<description><![CDATA[Product #1 description]]></description>
<g:image_link><![CDATA[https://data.ourwebsite.com/imgprodotto/product-1_big.jpg]]></g:image_link>
<g:sale_price>29.25 EUR</g:sale_price>
<g:price>65.00 EUR</g:price>
<g:shipping_weight>0.5 kg</g:shipping_weight>
<g:featured_product>y</g:featured_product>
<g:product_type><![CDATA[Product #1 category]]></g:product_type>
<g:availability>in stock</g:availability>
<g:availability_date>2022-08-10T00:00-0000</g:availability_date>
<qty>3</qty>
<g:payment_accepted>Visa</g:payment_accepted>
<g:payment_accepted>MasterCard</g:payment_accepted>
<g:payment_accepted>CartaSi</g:payment_accepted>
<g:payment_accepted>Aura</g:payment_accepted>
<g:payment_accepted>PayPal</g:payment_accepted>
</item>
<item>
<title><![CDATA[Product #2 title]]></title>
<g:brand><![CDATA[Product #2 brand]]></g:brand>
<g:mpn><![CDATA[573489547859]]></g:mpn>
<g:gender>Unisex</g:gender>
<g:age_group>Adult</g:age_group>
<g:size>Unica</g:size>
<g:condition>new</g:condition>
<g:id>fr_47362_382936</g:id>
<g:item_group_id>fr_47362</g:item_group_id>
<link><![CDATA[https://www.ourwebsite.com/product_2_url.htm?mid=168192]]></link>
<description><![CDATA[Product #2 description]]></description>
<g:image_link><![CDATA[https://data.ourwebsite.com/imgprodotto/product-2_big.jpg]]></g:image_link>
<g:sale_price>143.91 EUR</g:sale_price>
<g:price>159.90 EUR</g:price>
<g:shipping_weight>8.0 kg</g:shipping_weight>
<g:product_type><![CDATA[Product #2 category]]></g:product_type>
<g:availability>in stock</g:availability>
<g:availability_date>2022-08-10T00:00-0000</g:availability_date>
<qty>1</qty>
<g:payment_accepted>Visa</g:payment_accepted>
<g:payment_accepted>MasterCard</g:payment_accepted>
<g:payment_accepted>CartaSi</g:payment_accepted>
<g:payment_accepted>Aura</g:payment_accepted>
<g:payment_accepted>PayPal</g:payment_accepted>
</item>
...
</channel>
</rss>
I need to produce a XML file purged from all the tags inside <item> except for <g:mpn>, <link>, <g:sale_price> and <qty>.
In the example above, the result should be
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">
<channel>
<title>E-commerce's products.</title>
<description><![CDATA[Clothing and accessories.]]></description>
<link>https://www.ourwebsite.com/</link>
<item>
<g:mpn><![CDATA[5643785645]]></g:mpn>
<link><![CDATA[https://www.ourwebsite.com/product_1_url.htm?mid=62367]]></link>
<g:sale_price>29.25 EUR</g:sale_price>
<qty>3</qty>
</item>
<item>
<g:mpn><![CDATA[573489547859]]></g:mpn>
<link><![CDATA[https://www.ourwebsite.com/product_2_url.htm?mid=168192]]></link>
<g:sale_price>143.91 EUR</g:sale_price>
<qty>1</qty>
</item>
...
</channel>
</rss>
I've looked at SimpleXML, DOMDocument, XPath docs but I couldn't find the way to exclude specific elements. I don't want to select by name the nodes I have to delete, as in a future Google could add some nodes and they will not be deleted by my script.
I've also tried to loop through namespaced elements with SimpleXML and unset them if not matched with the nodes I have to keep:
$g = $element->children($namespaces['g']); //$element is the SimpleXMLElement of <item> tag
foreach ($g as $gchild) {
if ($gchild->getName() != "mpn") { //for example
unset($gchild);
}
}
but the code above doesn't remove all nodes except for <g:mpn>, for example.
PS: consider the fact that the XML contains both namespaced and not namespaced elements
Thank you in advance.
EDIT:
I've managed to do this with the following code:
$elementsToKeep = array("mpn", "link", "sale_price", "qty");
$domdoc = new DOMDocument();
$domdoc->preserveWhiteSpace = FALSE;
$domdoc->formatOutput = TRUE;
$domdoc->loadXML($myXMLDocument->asXML()); //$myXMLDocument is the SimpleXML document related to the original XML
$xpath = new DOMXPath($domdoc);
foreach ($element->children() as $child) {
$cname = $child->getName();
if (!in_array($cname, $elementsToKeep)) {
foreach($xpath->query('/rss/channel/item/'.$cname) as $node) {
$node->parentNode->removeChild($node);
}
}
}
$g = $element->children($namespaces['g']);
foreach ($g as $gchild) {
$gname = $gchild->getName();
if (!in_array($gname, $elementsToKeep)) {
foreach($xpath->query('/rss/channel/item/g:'.$gname) as $node) {
$node->parentNode->removeChild($node);
}
}
}
I've used DOMDocument and DOMXPath and two loops on no-namespaced tags and namespaced tags, in order to use the removeChild function of DOMDocument.
Really there is not a cleaner solution?? Thanks again
Somewhat simpler:
$items = $xpath->query('//item');
foreach($items as $item) {
$targets = $xpath->query('.//*',$item);
foreach($targets as $target) {
if (!in_array($target->localName, $elementsToKeep)) {
$target->parentNode->removeChild($target);
}
};
};
Use XPath to express all child elements you want to remove.
Then use the library of your choice to remove the elements.
SimpleXMLElement example:
$sxe = simplexml_load_string($xml);
foreach ($sxe->xpath('//item/*[
not(
name() = "g:mpn"
or name() = "link"
or name() = "g:sale_price"
or name() = "qty"
)
]') as $child) unset($child[0]);
echo $sxe->asXML(), "\n";
DOMDocument example:
This one is mainly identical to the previous example, with a bit of a variation on the xpath expression to explicitly use namespace URIs for the elements. This prevents that it breaks when the namespace prefix changes (it also works in the SimpleXMLElement example):
$doc = new DOMDocument();
$doc->loadXML($xml);
$xpath = new DOMXPath($doc);
foreach ($xpath->query('//item/*[
not(
(local-name() = "mpn" and namespace-uri() = "http://base.google.com/ns/1.0")
or (local-name() = "link" and namespace-uri() = "")
or (local-name() = "sale_price" and namespace-uri() = "http://base.google.com/ns/1.0")
or (local-name() = "qty" and namespace-uri() = "")
)
]') as $child) {
$child->parentNode->removeChild($child);
}
echo $doc->saveXML(), "\n";
I have an XML file in which one child has two categories, but with the same name. I want to add one title to each one. How can we do it in PHP?
This is my XML
<root>
<result>
<node>
<title> Some Title Name</title>
<categories>
<category> categor_one </category>
<category> categor_two </category>
</categories>
</node>
<node>
<title> Some Title Name</title>
<categories>
<category> categor_one </category>
<category> categor_tree </category>
</categories>
</node>
</result>
</root>
But I want to obtain this
<root>
<result>
<node>
<title> Some Title Name</title>
<category>categor_one///categor_two </category>
<category1>categor_one///categor_tree</category1>
</node>
</result>
</root>
I managed to impement a function that only gets correctly the category, but if the title is the same it doesn't work as it just creates a new one.
function solve_something($xml, $destination)
{
$xml = simplexml_load_file($xml, "SimpleXMLElement", LIBXML_NOCDATA);
$json = json_encode($xml);
$items = json_decode($json, TRUE);
$products = array();
$product_data = array();
foreach($items['result']['node'] as $item){
$product_data['title'] = $item['title'];
foreach ($item['categories'] as $category) {
if (is_array($category)) {
$product_data['category'] = implode('///', $category);
} else {
$product_data['category'] = $category;
}
}
$products[] = $product_data;
unset($product_data);
}
$path = createXML($products, $destination);
return $path;
}
function createXML($data, $destination)
{
$xmlDoc = new DOMDocument('1.0', 'UTF-8');
$root = $xmlDoc->appendChild($xmlDoc->createElement("root"));
foreach ($data as $key => $product) {
$productA = $root->appendChild($xmlDoc->createElement('product'));
foreach ($product as $key1 => $val) {
if (!empty($val)) {
if ($key1 == 'price' || $key1 == 'tax' || $key1 == 'stockAmount') {
$productA->appendChild($xmlDoc->createElement($key1, $val));
} else {
$ProductKey = $productA->appendChild($xmlDoc->createElement($key1));
$ProductKey->appendChild($xmlDoc->createCDATASection($val));
}
}
}
}
$xmlDoc->formatOutput = true;
fn_rm($destination);
$xmlDoc->save($destination);
return $destination;
}
The output of my code is something like this
<root>
<product>
<title> Some Title Name</title>
<category>categor_one///categor_two </category>
</product>
<product>
<title> Some Title Name</title>
<category>categor_one///categor_tree</category>
</product>
</root>
If you want to keep the data together with the same title, one approach could be to collect that data upfront by using the title as an array key (if it is a valid array key)
When you create the xml, you have the title in the outer foreach loop as the key, and in the inner foreach you can create elements using implode.
Note that in your code you started using product so I took that as a node name.
Example code, which you could use in your code:
$products = array();
$product_data = array();
$xml = simplexml_load_file($xml, "SimpleXMLElement", LIBXML_NOCDATA);
foreach ($xml->result->node as $node) {
$product_data['title'] = (string)$node->title;
foreach($node->categories as $category) {
if (is_array($category)) {
$product_data['category'] = implode('///', $category);
continue;
}
$product_data['category'] = (array)$category->category;
}
$products[(string)$node->title][] = $product_data;
}
$xmlDoc = new DOMDocument('1.0', 'UTF-8');
$root = $xmlDoc->appendChild($xmlDoc->createElement("root"));
foreach ($products as $key => $product) {
$productA = $root->appendChild($xmlDoc->createElement('product'));
$productA->appendChild($xmlDoc->createElement("title", $key));
for ($i = 0; $i < count($product); $i++) {
$productA->appendChild($xmlDoc->createElement("category" . $i, implode("///", $product[$i]["category"])));
}
}
$xmlDoc->formatOutput = true;
echo $xmlDoc->saveXML();
Output
<?xml version="1.0" encoding="UTF-8"?>
<root>
<product>
<title> Some Title Name</title>
<category0> categor_one /// categor_two </category0>
<category1> categor_one /// categor_tree </category1>
</product>
</root>
Php demo
I've rewritten a script that used the PHP DOM functions to iterate through an XML file with a structure like this:
<file>
<record>
<Source>
<SourcePlace>
<Country>Germany</Country>
</SourcePlace>
</Source>
<Person>
<Name>
<firstname>John</firstname>
<lastname>Doe<lastname>
</Name>
</Person>
</record>
<record>
..
</record>
</file>
I've replaced it with a script that uses XMLreader to find each separate record and turn that into a DOMdocument after which it is iterated through. Iteration was done by checking if the node had a child:
function findLeaves($node) {
echo "nodeType: ".$node->nodeType.", nodeName:". $node->nodeName."\n";
if($node->hasChildNodes() ) {
foreach($node->childNodes as $element) {
findLeaves($element)
}
}
ELSE { <do something with leave> }
}
The problem is that the behaviour of the findLeaves() function has changed between the two. Under DOM a node without a value (like Source) had no #text childnodes. Output of above would be:
nodeType:1, nodeName:Source
nodeType:1, nodeName:SourcePlace
nodeType:1, nodeName:Country
nodeType:3, nodeName:#text ```
Under XMLreader this becomes:
nodeType: 1, nodeName:Source
nodeType: 3, nodeName:#text
nodeType: 1, nodeName:SourcePlace
nodeType: 3, nodeName:#text
nodeType: 1, nodeName:Country
I've checked the saveXML() result of the data before entering this function but it seems identical, barring some extra spaces. What could be the reason for the difference?
Code loading the file before the findleaves() function under DOM:
$xmlDoc = new DOMDocument();
$xmlDoc->preserveWhiteSpace = false;
$xmlDoc->load($file);
$xpath = new DOMXPath($xmlDoc);
$records = $xpath->query('//record');
foreach($records as $record) {
foreach ($xpath->query('.//Source', $record) as $source_record) {
findleaves($source_record);
}
}
Code loading the file before the findleaves() function under XMLreader:
$xmlDoc = new XMLReader()
$xmlDoc->open($file)
while ($xmlDoc->read() ) {
if ($xmlDoc->nodeType == XMLReader::ELEMENT && $xmlDoc->name == 'record') {
$record_node = $xmlDoc->expand();
$recordDOM = new DomDocument();
$n = $recordDOM->importNode($record_node,true);
$recordDOM->appendChild($n);document
$recordDOM->preserveWhiteSpace = false;
$xpath = new DOMXPath($recordDOM);
$records = $xpath->query('//record');
foreach($records as $record) {
foreach ($xpath->query('.//Source', $record) as $source_record) {
findleaves($source_record);
}
}
The property DOMDocument::$preserveWhiteSpace affects the load/parse functions. So if you use XMLReader::expand() the property of the document has no effect - you do not load a XML string into it.
You're using Xpath already. .//*[not(*) and normalize-space(.) !== ""] will select element nodes without element children and without any text content (expect white spaces).
Here is an example (including other optimizations):
$xml = <<<'XML'
<file>
<record>
<Source>
<SourcePlace>
<Country>Germany</Country>
</SourcePlace>
</Source>
<Person>
<Name>
<firstname>John</firstname>
<lastname>Doe</lastname>
</Name>
</Person>
</record>
</file>
XML;
$reader = new XMLReader();
$reader->open('data://text/plain;base64,'.base64_encode($xml));
$document = new DOMDocument();
$xpath = new DOMXpath($document);
// find first record
while ($reader->read() && $reader->localName !== 'record') {
continue;
}
while ($reader->localName === 'record') {
// expand node into prepared document
$record = $reader->expand($document);
// match elements without child elements and empty text content
// ignore text nodes with only white space
$expression = './Source//*[not(*) and normalize-space() != ""]';
foreach ($xpath->evaluate($expression, $record) as $leaf) {
var_dump($leaf->localName, $leaf->textContent);
}
// move to the next record sibling
$reader->next('record');
}
$reader->close();
Output:
string(7) "Country"
string(7) "Germany"
I really need your help who works with XML and PHP. Looked for many other questions, but still nothing was found about my situation when in xml there is deeper fields and I can't grab them to csv output (code below).
<product>
<images>
<image>...</image>
<image>...</image>
</images>
</product>
My XML file looks like this:
<root>
<product>
<url>
<![CDATA[
https://
]]>
</url>
<id>185</id>
<barcode>284</barcode>
<categories>
<category>14</category>
<category>2</category>
</categories>
<title>
<![CDATA[ Product1 ]]>
</title>
<description>
<![CDATA[
<p>description</p>
]]>
</description>
<price>10</price>
<sec_costs>13.000000</sec_costs>
<quantity>10</quantity>
<warranty/>
<weight>0.000000</weight>
<delivery_text>
<![CDATA[ 1 - 2 d. ]]>
</delivery_text>
<manufacturer>
<![CDATA[ ]]>
</manufacturer>
<images>
<image>
<![CDATA[
https://test.eu/r.jpg
]]>
</image>
<image>
<![CDATA[
https://test.eu/er.jpg
]]>
</image>
<image>
<![CDATA[
https://test.eu/eer.jpg
]]>
</image>
</images>
<product_with_gift>
<![CDATA[ False ]]>
</product_with_gift>
<barcode_format>
<![CDATA[ EAN ]]>
</barcode_format>
</product>
I am using this code to convert it from XML to CSV (used it from other member), the problem is the code works fine, but it doesn't grab images (tried replacing image with images, added extra images columns, but nothing worked out, it just doesn't grab links to image files:
<?
$filexml = 'imp2.xml';
$xml = simplexml_load_file($filexml);
$xml->registerXPathNamespace('g', 'http://base.google.com/ns/1.0');
if (file_exists($filexml)) {
$xml = simplexml_load_file($filexml);
$i = 1; // Position counter
$values = []; // PHP array
// Writing column headers
$columns = array('id', 'barcode', 'title', 'description', 'price', 'sec_costs', 'quantity', 'warranty', 'weight', 'delivery_text', 'manufacturer', 'image', 'product_with_gift', 'barcode_format');
$fs = fopen('csv.csv', 'w');
fputcsv($fs, $columns);
fclose($fs);
// Iterate through each <product> node
$node = $xml->xpath('//product');
foreach ($node as $n) {
// Iterate through each child of <item> node
foreach ($columns as $col) {
if (count($xml->xpath('//product['.$i.']/'.$col)) > 0) {
$values[] = trim($xml->xpath('//product['.$i.']/'.$col)[0]);
} else {
$values[] = '';
}
}
// Write to CSV files (appending to column headers)
$fs = fopen('csv.csv', 'a');
fputcsv($fs, $values);
fclose($fs);
$values = []; // Clean out array for next <item> (i.e., row)
$i++; // Move to next <item> (i.e., node position)
}
}
?>
Any solutions from mid, premium xml,php?
The problem is that you are trying to fetch a list of nodes using just the images tag as the start point, as the subnodes have their own content, they will not appear in the higher level nodes text.
I've made a few changes to the code, but also I now use the <image> element to fetch the data. This code doesn't assume it's just one node for each item, so when it uses the XPath, it always loops through all items and build them into a single string before adding them to the CSV.
$filexml = 'imp2.xml';
if (file_exists($filexml)) {
// Only open file once you know it exists
$xml = simplexml_load_file($filexml);
$i = 1; // Position counter
$values = []; // PHP array
// Writing column headers
$columns = array('id', 'barcode', 'title', 'description', 'price', 'sec_costs', 'quantity', 'warranty', 'weight', 'delivery_text', 'manufacturer', 'image', 'product_with_gift', 'barcode_format');
// Open output file at start
$fs = fopen('csv.csv', 'w');
fputcsv($fs, $columns);
// Iterate through each <product> node
$node = $xml->xpath('//product');
foreach ($node as $n) {
// Iterate through each child of <item> node
foreach ($columns as $col) {
// Use //'.$col so node doesn't have to be directly under product
$dataMatch = $xml->xpath('//product['.$i.']//'.$col);
if (count($dataMatch) > 0) {
// Build list of all matches
$newData = '';
foreach ( $dataMatch as $data) {
$newData .= trim((string)$data).",";
}
// Remove last comma before adding it in
$values[] = rtrim($newData, ",");
} else {
$values[] = '';
}
}
fputcsv($fs, $values);
$values = []; // Clean out array for next <item> (i.e., row)
$i++; // Move to next <item> (i.e., node position)
}
// Close file only at end
fclose($fs);
}
There is an XML file with a content similar to the following:
<FMPDSORESULT xmlns="http://www.filemaker.com">
<ERRORCODE>0</ERRORCODE>
<DATABASE>My_Database</DATABASE>
<LAYOUT/>
<ROW MODID="1" RECORDID="1">
<Name>John</Name>
<Age>19</Age>
</ROW>
<ROW MODID="2" RECORDID="2">
<Name>Steve</Name>
<Age>25</Age>
</ROW>
<ROW MODID="3" RECORDID="3">
<Name>Adam</Name>
<Age>45</Age>
</ROW>
I tried to sort the ROW tags by the values of Name tags using array_multisort function:
$xml = simplexml_load_file( 'xml1.xml');
$xml2 = sort_xml( $xml );
print_r( $xml2 );
function sort_xml( $xml ) {
$sort_temp = array();
foreach ( $xml as $key => $node ) {
$sort_temp[ $key ] = (string) $node->Name;
}
array_multisort( $sort_temp, SORT_DESC, $xml );
return $xml;
}
But the code doesn't work as expected.
I would recommend using the DOM extension, as it is more flexible:
$doc = new DOMDocument();
$doc->preserveWhiteSpace = false;
$doc->formatOutput = true;
$doc->load('xml1.xml');
// Get the root node
$root = $doc->getElementsByTagName('FMPDSORESULT');
if (!$root->length)
die('FMPDSORESULT node not found');
$root = $root[0];
// Pull the ROW tags from the document into an array.
$rows = [];
$nodes = $root->getElementsByTagName('ROW');
while ($row = $nodes->item(0)) {
$rows []= $root->removeChild($row);
}
// Sort the array of ROW tags
usort($rows, function ($a, $b) {
$a_name = $a->getElementsByTagName('Name');
$b_name = $b->getElementsByTagName('Name');
return ($a_name->length && $b_name->length) ?
strcmp(trim($a_name[0]->textContent), trim($b_name[0]->textContent)) : 0;
});
// Append ROW tags back into the document
foreach ($rows as $row) {
$root->appendChild($row);
}
// Output the result
echo $doc->saveXML();
Output
<?xml version="1.0"?>
<FMPDSORESULT xmlns="http://www.filemaker.com">
<ERRORCODE>0</ERRORCODE>
<DATABASE>My_Database</DATABASE>
<LAYOUT/>
<ROW MODID="3" RECORDID="3">
<Name>Adam</Name>
<Age>45</Age>
</ROW>
<ROW MODID="1" RECORDID="1">
<Name>John</Name>
<Age>19</Age>
</ROW>
<ROW MODID="2" RECORDID="2">
<Name>Steve</Name>
<Age>25</Age>
</ROW>
</FMPDSORESULT>
Regarding XPath
You can use DOMXPath for even more flexible traversing. However, in this specific problem the use of DOMXPath will not bring significant improvements, in my opinion. Anyway, I'll give examples for completeness.
Fetching the rows:
$xpath = new DOMXPath($doc);
$xpath->registerNamespace('myns', 'http://www.filemaker.com');
$rows = [];
foreach ($xpath->query('//myns:ROW') as $row) {
$rows []= $row->parentNode->removeChild($row);
}
Appending the rows back into the document:
$root = $xpath->evaluate('/myns:FMPDSORESULT')[0];
foreach ($rows as $row) {
$root->appendChild($row);
}
Some SimpleXMLElement methods return arrays but most return SimpleXMLElement objects which implement Iterator. A var_dump() will only show part of of the data in a simplified representation. However it is an object structure, not a nested array.
If I understand you correctly you want to sort the ROW elements by the Name child. You can fetch them with the xpath() method, but you need to register a prefix for the namespace. It returns an array of SimpleXMLElement objects. The array can be sorted with usort.
$fResult = new SimpleXMLElement($xml);
$fResult->registerXpathNamespace('fm', 'http://www.filemaker.com');
$rows = $fResult->xpath('//fm:ROW');
usort(
$rows,
function(SimpleXMLElement $one, SimpleXMLElement $two) {
return strcasecmp($one->Name, $two->Name);
}
);
var_dump($rows);
In DOM that will not look much different, but DOMXpath::evaluate() return a DOMNodeList. You can convert it into an array using iterator_to_array.
$document = new DOMDocument();
$document->loadXml($xml);
$xpath = new DOMXpath($document);
$xpath->registerNamespace('fm', 'http://www.filemaker.com');
$rows = iterator_to_array($xpath->evaluate('//fm:ROW'));
usort(
$rows,
function(DOMElement $one, DOMElement $two) use ($xpath) {
return strcasecmp(
$xpath->evaluate('normalize-space(Name)', $one),
$xpath->evaluate('normalize-space(Name)', $two)
);
}
);
var_dump($rows);
DOM has no magic methods to access children and values, Xpath can be used to fetch them. The Xpath function string() converts the first node into a string. It return an empty string if the node list is empty. normalize-space() does a little more. It replaces all groups of whitespaces with a single space and strips it from the start and end of the string.