Looping through SimpleXMLElement to access attributes - php

I am trying process data retrieved with SimpleXML and am having great difficulty. I have read numerous threads here about this subject, they all LOOK like what I am doing, but mine are not working. Here's what I've got:
<ROOT>
<ROWS COMP_ID="165462">
<ROWS COMP_ID="165463">
</ROOT>
My code:
$xml = simplexml_load_file('10.xml');
foreach( $xml->ROWS as $comp_row ) {
$id = $comp_row->COMP_ID;
}
As I step through this in my debugger, I can see that $id is not set to the string value of COMP_ID, but becomes a SimpleXMLElement itself containing the CLASSNAME object. I've tried many variations of addressing this attribute but none work, including $comp_row->attributes()->COMP_ID and others.
What am I missing?

SimpleXML is an array-like object. Cheat sheet:
Unprefixed child elements as numeric-index or traversable
Does not include prefixed elements (NOTE, I really mean prefixed, not null-namespace! SimpleXMLElement handling of namespaces is a strange and arguably broken.)
first child: $sxe[0]
new SimpleXMLElement with a subset of matching elements: $sxe->ROWS, $sxe->{'ROWS'}
iterate children: foreach ($sxe as $e), $sxe->children()
Text content: (string) $sxe. SimpleXMLElement always returns another SimpleXMLElement, so if you need a string cast it explicitly!
Prefixed child elements:
$sxe->children('http://example.org') returns a new SimpleXMLElement with elements
in the matching namespace, with namespace stripped so you can use it like the previous section.
Attributes in null namespace as key-index:
specific attribute: `$sxe['attribute-name']
all attributes: $sxe->attributes()
$sxe->attributes() returns a special SimpleXMLElement that shows attributes as both child elements and attributes, so both the following work:
$sxe->attributes()->COMP_ID
$a = $sxe->attributes(); $a['COMP_ID'];
Value of an attribute: coerce to string (string) $sxe['attr-name']
Attributes in other namespaces:
all attributes: $sxe->attributes('http://example.org')
specific attribute: $sxe_attrs = $sxe->attributes('http://example.org'); $sxe_attrs['attr-name-without-prefix']
What you want is:
$xml = '<ROOT><ROWS COMP_ID="165462"/><ROWS COMP_ID="165463"/></ROOT>';
$sxe = simplexml_load_string($xml);
foreach($sxe->ROWS as $row) {
$id = (string) $row['COMP_ID'];
}

You're missing...
foreach( $xml->ROWS as $comp_row ) {
foreach ($comp_row->attributes() as $attKey => $attValue) {
// i.e., on first iteration: $attKey = 'COMP_ID', $attValue = '165462'
}
}
PHP Manual: SimpleXMLElement::attributes

Related

PHP: How to force simplexml to use certain datatype for a node

How can one enforce simplexml_load_string( ) to use same data structure at each node point.
$xml = "
<level1>
<level2>
<level3>Hello</level3>
<level3>stackoverflow</level3>
</level2>
<level2>
<level3>My problem</level3>
</level2>
</level1>";
$xmlObj = simplexml_load_string($xml)
var_dump($xmlObj);
Examining the output,
level1 is an object; level2 is an array; level2[0] is an array.
level2[1] is an object, because there's only one child node, which I'll rather have as a single index array.
I'm collecting the xml from user, and there may be 1 or more nodes inside each level2. My sanitisation block is a foreach loop which fails when there's only one node inside level2.
The sanitation block looks something like this
foreach($xmlObj -> level2 as $lvl2){
if($lvl2 -> level3[0] == 'condition'){ doSomething( ); }
}
doSomething() works fine when <level2> always has more than one child node in the xml string. If <level2> has only one child <level3> node, an error about trying to get attribute of a non-object comes up.
var_dump shows that the data type changes from object to array depending on how many nodes are nested within.
I'll prefer a way to ensure <level2> to always be an array regardless of how many children are within. That saves me from editing too much. But any other way out would suffice.
Thanks
It is not an information available in the XML itself. So you will have to add it in your implementation. SimpleXML provides both list and item access to a child elements. If you access it as a list (for example with foreach) it will provide all matching child elements.
$xml = "
<level1>
<level2>
<level3>Hello</level3>
<level3>stackoverflow</level3>
</level2>
<level2>
<level3>My problem</level3>
</level2>
</level1>";
$level1 = new SimpleXMLElement($xml);
$result = [];
foreach($level1->level2 as $level2) {
$data2 = [];
foreach ($level2->level3 as $level3) {
$data2[] = (string)$level3;
}
$result[] = $data2;
}
var_dump($result);
So the trick is to use the SimpleXMLElement instance directly and not convert it into an array. Do not treat the creation of your JSON structure as a generic conversion. Build up a specific output while reading the XML using SimpleXML.

PHP - Unable to parse attribute using SimpleXML

Given the following xml:
<data xmlns:ns2="...">
<versions>
<ns2:version type="HW">E</ns2:version>
<ns2:version type="FW">3160</ns2:version>
<ns2:version type="SW">3.4.1 (777)</ns2:version>
</versions>
...
</data>
I am trying to parse the third attribute ~ns2:version type="SW" but when running the following code I get nothing..
$s = simplexml_load_file('data.xml');
echo $s->versions[2]->{'ns2:version'};
Running this gives the following output:
$s = simplexml_load_file('data.xml');
var_dump($s->versions);
How can I properly get that attribute?
You've got some quite annoying XML to work with there, at least as far as SimpleXML is concerned.
Your version elements are in the ns2 namespace, so in order to loop over them, you need to do something like this:
$s = simplexml_load_string($xml);
foreach ($s->versions[0]->children('ns2', true)->version as $child) {
...
}
The children() method returns all children of the current tag, but only in the default namespace. If you want to access elements in other namespaces, you can pass the local alias and the second argument true.
The more complicated part is that the type attributes is not considered to be part of this same namespace. This means you can't use the standard $element['attribute'] form to access it, since your element and attribute are in different namespaces.
Fortunately, SimpleXML's attributes() method works in the same way as children(), and so to access the attributes in the global namespace, you can pass it an empty string:
$element->attributes('')->type
In full, this is:
$s = simplexml_load_string($xml);
foreach ($s->versions[0]->children('ns2', true)->version as $child) {
echo (string) $child->attributes()->type, PHP_EOL;
}
This will get you the output
HW
FW
SW
To get the third attribute.
$s = simplexml_load_file('data.xml');
$sxe = new SimpleXMLElement($s);
foreach ($sxe as $out_ns) {
$ns = $out_ns->getNamespaces(true);
$child = $out_ns->children($ns['ns2']);
}
echo $child[2];
Out put:
3.4.1 (777)

How to convert XML attributes to text nodes

I have a PHP script that pulls an XML file from a remote server, and converts it to JSON using simplexml_load_string and json_encode. However, the simplexml_load_string seems to ignore inline attributes, like so:
<AxisFeedrate dataItemId="iid7" timestamp="2012-03-21T15:15:41-04:00" sequence="7" name="Yfrt" subType="ACTUAL" units="MILLIMETER/SECOND">UNAVAILABLE</AxisFeedrate>
In this case the JSON representation would be {AxisFeedrate: 'UNAVAILABLE'}
However, I need to have those attributes available. One idea I've been approaching is replacing strings to turn the attributes into text nodes like so:
<AxisFeedrate>
<dataItemId>iid7</dataItemId>
<timestamp>2012-03-21T15:15:41-04:00</timestamp>
<sequence>7</sequence>
<name>Yfrt</name>
<subType>ACTUAL</subType>
<units>MILLIMETER/SECOND"</units>
<value>UNAVAILABLE</value>
</AxisFeedrate>
I can turn the attributes into their own tag elements with regular find/replace, but I'm having trouble wrapping the original text value in a Value tag, at least with find/replace.
What are some good approaches for doing this? The above chunk of XML is in the middle of many similar chunks on different data items, so I couldn't just start by replacing the first closing > with >...
You could use SimpleXML itself to read the attributes.
Example:
<?php
$xml=simplexml_load_string('<AxisFeedrate dataItemId="iid7" timestamp="2012-03-21T15:15:41-04:00" sequence="7" name="Yfrt" subType="ACTUAL" units="MILLIMETER/SECOND">UNAVAILABLE</AxisFeedrate>');
foreach($xml->attributes() as $k=>$v) {
echo $k." -> ".(string)$v."\n";
}
?>
Output:
dataItemId -> iid7
timestamp -> 2012-03-21T15:15:41-04:00
sequence -> 7
name -> Yfrt
subType -> ACTUAL
units -> MILLIMETER/SECOND
Try this regex: ([\w]*?)="(.*?)" with this replace <$1>$2</$1>\n
You should use SimpleXML. Be aware though, that you have to cast values to string type explicitly, or you'll get objects.
$xml_string = <<<XML
<AxisFeedrate
dataItemId="iid7"
timestamp="2012-03-21T15:15:41-04:00"
sequence="7"
name="Yfrt"
subType="ACTUAL"
units="MILLIMETER/SECOND"
>UNAVAILABLE</AxisFeedrate>
XML;
$xml = simplexml_load_string($xml_string);
$axis_info = array('value' => (string)$xml);
foreach($xml -> attributes() as $attr => $val) {
$axis_info[$attr] = (string) $val;
}
echo json_encode(array("AxisFeedrate" => $axis_info));
Update:
This will give you a more generic version, but notice that the attributes are cast as an array and that this only works on a single element:
$xml_string = <<<XML
<AxisFeedrate dataItemId="iid7" timestamp="2012-03-21T15:15:41-04:00" sequence="7" name="Yfrt" subType="ACTUAL" units="MILLIMETER/SECOND">UNAVAILABLE</AxisFeedrate>
XML;
$xml = simplexml_load_string($xml_string);
$obj_name = $xml -> getName();
$attributes = (array) $xml->attributes();
$axis_info[$obj_name] = $attributes["#attributes"];
$axis_info[$obj_name]['value'] = (string) $xml;
echo json_encode($axis_info);

How to get values inside <![CDATA[values]] > using php DOM?

How can i get values inside <![CDATA[values]] > using php DOM.
This is few code from my xml.
<Destinations>
<Destination>
<![CDATA[Aghia Paraskevi, Skiatos, Greece]]>
<CountryCode>GR</CountryCode>
</Destination>
<Destination>
<![CDATA[Amettla, Spain]]>
<CountryCode>ES</CountryCode>
</Destination>
<Destination>
<![CDATA[Amoliani, Greece]]>
<CountryCode>GR</CountryCode>
</Destination>
<Destination>
<![CDATA[Boblingen, Germany]]>
<CountryCode>DE</CountryCode>
</Destination>
</Destinations>
Working with PHP DOM is fairly straightforward, and is very similar to Javascript's DOM.
Here are the important classes:
DOMNode — The base class for anything that can be traversed inside an XML/HTML document, including text nodes, comment nodes, and CDATA nodes
DOMElement — The base class for tags.
DOMDocument — The base class for documents. Contains the methods to load/save XML, as well as normal DOM document methods (see below).
There are a few staple methods and properties:
DOMDocument->load() — After creating a new DOMDocument, use this method on that object to load from a file.
DOMDocument->getElementsByTagName() — this method returns a node list of all elements in the document with the given tag name. Then you can iterate (foreach) on this list.
DOMNode->childNodes — A node list of all children of a node. (Remember, a CDATA section is a node!)
DOMNode->nodeType — Get the type of a node. CDATA nodes have type XML_CDATA_SECTION_NODE, which is a constant with the value 4.
DOMNode->textContent — get the text content of any node.
Note: Your CDATA sections are malformed. I don't know why there is an extra ]] in the first one, or an unclosed CDATA section at the end of the line, but I think it should simply be:
<![CDATA[Aghia Paraskevi, Skiatos, Greece]]>
Putting this all together we:
Create a new document object and load the XML
Get all Destination elements by tag name and iterate over the list
Iterate over all child nodes of each Destination element
Check if the node type is XML_CDATA_SECTION_NODE
If it is, echo the textContent of that node.
Code:
$doc = new DOMDocument();
$doc->load('test.xml');
$destinations = $doc->getElementsByTagName("Destination");
foreach ($destinations as $destination) {
foreach($destination->childNodes as $child) {
if ($child->nodeType == XML_CDATA_SECTION_NODE) {
echo $child->textContent . "<br/>";
}
}
}
Result:
Aghia Paraskevi, Skiatos, Greece
Amettla, Spain
Amoliani, Greece
Boblingen, Germany
Use this:
$parseFile = simplexml_load_file($myXML,'SimpleXMLElement', LIBXML_NOCDATA)
and next :
foreach ($parseFile->yourNode as $node ){
etc...
}
Best and easy way
$xml = simplexml_load_string($xmlData, 'SimpleXMLElement', LIBXML_NOCDATA);
$xmlJson = json_encode($xml);
$xmlArr = json_decode($xmlJson, 1); // Returns associative array
Use replace CDATA before parsing PHP DOM element after that you can get the innerXml or innerHtml:
str_replace(array('<\![CDATA[',']]>'), '', $xml);
I use following code.
Its not only read all xml data with
<![CDATA[values]] >
but also convert xml object to php associative array. So we can apply loop on the data.
$xml_file_data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA),true), true);
Hope this will work for you.
function inBetweenOf(string $here, string $there, string $content) : string {
$left_over = strlen(substr($content, strpos($content, $there)));
return substr($content, strpos($content, $here) + strlen($here), -$left_over);
}
Iterate over "Destination" tags and then call inBetweenOf on each iteration.
$doc = inBetweenOf('<![CDATA[', ']]>', $xml);

In SimpleXML, how can I add an existing SimpleXMLElement as a child element?

I have a SimpleXMLElement object $child, and a SimpleXMLElement object $parent.
How can I add $child as a child of $parent? Is there any way of doing this without converting to DOM and back?
The addChild() method only seems to allow me to create a new, empty element, but that doesn't help when the element I want to add $child also has children. I'm thinking I might need recursion here.
Unfortunately SimpleXMLElement does not offer anything to bring two elements together. As #nickf wrote, it's more fitting for reading than for manipulation. However, the sister extension DOMDocument is for editing and you can bring both together via dom_import_simplexml(). And #salathe shows in a related answer how this works for specific SimpleXMLElements.
The following shows how this work with input checking and some more options. I do it with two examples. The first example is a function to insert an XML string:
/**
* Insert XML into a SimpleXMLElement
*
* #param SimpleXMLElement $parent
* #param string $xml
* #param bool $before
* #return bool XML string added
*/
function simplexml_import_xml(SimpleXMLElement $parent, $xml, $before = false)
{
$xml = (string)$xml;
// check if there is something to add
if ($nodata = !strlen($xml) or $parent[0] == NULL) {
return $nodata;
}
// add the XML
$node = dom_import_simplexml($parent);
$fragment = $node->ownerDocument->createDocumentFragment();
$fragment->appendXML($xml);
if ($before) {
return (bool)$node->parentNode->insertBefore($fragment, $node);
}
return (bool)$node->appendChild($fragment);
}
This exemplary function allows to append XML or insert it before a certain element, including the root element. After finding out if there is something to add, it makes use of DOMDocument functions and methods to insert the XML as a document fragment, it is also outlined in How to import XML string in a PHP DOMDocument. The usage example:
$parent = new SimpleXMLElement('<parent/>');
// insert some XML
simplexml_import_xml($parent, "\n <test><this>now</this></test>\n");
// insert some XML before a certain element, here the first <test> element
// that was just added
simplexml_import_xml($parent->test, "<!-- leave a comment -->\n ", $before = true);
// you can place comments above the root element
simplexml_import_xml($parent, "<!-- this works, too -->", $before = true);
// but take care, you can produce invalid XML, too:
// simplexml_add_xml($parent, "<warn><but>take care!</but> you can produce invalid XML, too</warn>", $before = true);
echo $parent->asXML();
This gives the following output:
<?xml version="1.0"?>
<!-- this works, too -->
<parent>
<!-- leave a comment -->
<test><this>now</this></test>
</parent>
The second example is inserting a SimpleXMLElement. It makes use of the first function if needed. It basically checks if there is something to do at all and which kind of element is to be imported. If it is an attribute, it will just add it, if it is an element, it will be serialized into XML and then added to the parent element as XML:
/**
* Insert SimpleXMLElement into SimpleXMLElement
*
* #param SimpleXMLElement $parent
* #param SimpleXMLElement $child
* #param bool $before
* #return bool SimpleXMLElement added
*/
function simplexml_import_simplexml(SimpleXMLElement $parent, SimpleXMLElement $child, $before = false)
{
// check if there is something to add
if ($child[0] == NULL) {
return true;
}
// if it is a list of SimpleXMLElements default to the first one
$child = $child[0];
// insert attribute
if ($child->xpath('.') != array($child)) {
$parent[$child->getName()] = (string)$child;
return true;
}
$xml = $child->asXML();
// remove the XML declaration on document elements
if ($child->xpath('/*') == array($child)) {
$pos = strpos($xml, "\n");
$xml = substr($xml, $pos + 1);
}
return simplexml_import_xml($parent, $xml, $before);
}
This exemplary function does normalize list of elements and attributes like common in Simplexml. You might want to change it to insert multiple SimpleXMLElements at once, but as the usage example shows below, my example does not support that (see the attributes example):
// append the element itself to itself
simplexml_import_simplexml($parent, $parent);
// insert <this> before the first child element (<test>)
simplexml_import_simplexml($parent->children(), $parent->test->this, true);
// add an attribute to the document element
$test = new SimpleXMLElement('<test attribute="value" />');
simplexml_import_simplexml($parent, $test->attributes());
echo $parent->asXML();
This is a continuation of the first usage-example. Therefore the output now is:
<?xml version="1.0"?>
<!-- this works, too -->
<parent attribute="value">
<!-- leave a comment -->
<this>now</this><test><this>now</this></test>
<!-- this works, too -->
<parent>
<!-- leave a comment -->
<test><this>now</this></test>
</parent>
</parent>
I hope this is helpful. You can find the code in a gist and as online demo / PHP version overview.
I know this isn't the most helpful answer, but especially since you're creating/modifying XML, I'd switch over to using the DOM functions. SimpleXML's good for accessing simple documents, but pretty poor at changing them.
If SimpleXML is treating you kindly in all other places and you want to stick with it, you still have the option of jumping over to the DOM functions temporarily to perform what you need to and then jump back again, using dom_import_simplexml() and simplexml_import_dom(). I'm not sure how efficient this is, but it might help you out.
Actually, it's possible (dynamically) if you look carefully on how addChild() is defined. I used this technique to convert any array into XML using recursion and pass-by-reference
addChild() returns SimpleXMLElement of added child.
to add leaf node, use $xml->addChilde($nodeName, $nodeValue).
to add a node which may have subnode or value, use
$xml->addChilde($nodeName), no value is passed to addChild(). This
will result in having a subnode of type SimpleXMLElement! not a
string!
target XML
<root>
<node>xyz</node>
<node>
<node>aaa</node>
<node>bbb</node>
</node>
</root>
Code:
$root = new SimpleXMLElement('<root />');
//add child with name and string value.
$root.addChild('node', 'xyz');
//adds child with name as root of new SimpleXMLElement
$sub = $root->addChild('node');
$sub.addChild('node', 'aaa');
$sub.addChild('node', 'bbb');
Leaving this here as I just stumbled upon this page and found that SimpleXML now supports this functionality through the ::addChild method.
You can use this method to do add any cascading elements as well:
$xml->addChild('parent');
$xml->parent->addChild('child');
$xml->parent->child->addChild('child_id','12345');

Categories