I need to return a SimpleXML object converted as a JSON object to work with it in JavaScript. The problem is that there are no attributes on any object with a value.
As an example:
<customer editable="true" maxChars="9" valueType="numeric">69236</customer>
becomes in the SimpleXML object:
"customer":"69236"
Where is the #attributes object?
This has driven me crazy on several occasions. When SimpleXML encounters a node that only has a text value, it drops all the attributes. My workaround has been to modify the XML prior to parsing with SimpleXML. With a bit of regular expressions, you can create a child node that contains the actual text value. For example, in your situation you can change the XML to:
<customer editable="true" maxChars="9" valueType="numeric"><value>69236<value></customer>
Some example code assuming that your XML string was in $str:
$str = preg_replace('/<customer ([^>]*)>([^<>]*)<\/customer>/i', '<customer $1><value>$2</value></customer>', $str);
$xml = #simplexml_load_string($str);
That would preserve the attributes and nest the text value in a child node.
I realize this is an old post, but in case it proves useful. The below extends #ryanmcdonnell's solution to work on any tags instead of a hard-coded tag. Hopefully it helps someone.
$str = preg_replace('/<([^ ]+) ([^>]*)>([^<>]*)<\/\\1>/i', '<$1 $2><value>$3</value></$1>', $result);
The main different is that it replaces /<customer with /<([^ ]+), and then </customer> with </\\1>
which tells it to match that part of the search against the first element in the pattern.
Then it just adjusts the placeholders ($1,$2,$3) to account for the fact that there are three sub-matches now instead of two.
So it appears that this is a bug and is fixed in PHP 7.4.5.
It's an old question, but I found something that works neat - parse it into a DOMNode object.
// $customer contains the SimpleXMLElement
$customerDom = dom_import_simplexml($customer);
var_dump($customerDom->getAttribute('numeric'));
Will show:
string 'numeric'
Here's some code to iterate through attributes, and construct JSON. If supports, one or many customers.
If you're XML looks like this (or just one customer)
<xml>
<customer editable="true" maxChars="9" valueType="numeric">69236</customer>
<customer editable="true" maxChars="9" valueType="numeric">12345</customer>
<customer editable="true" maxChars="9" valueType="numeric">67890</customer>
</xml>
Iterate through it like this.
try {
$xml = simplexml_load_file( "customer.xml" );
// Find the customer
$result = $xml->xpath('/xml/customer');
$bFirstElement = true;
echo "var customers = {\r\n";
while(list( , $node) = each($result)) {
if( $bFirstElement ) {
echo "'". $node."':{\r\n";
$bFirstElement = false;
} else {
echo ",\r\n'". $node."':{\r\n";
}
$bFirstAtt = true;
foreach($node->attributes() as $a => $b) {
if( $bFirstAtt ) {
echo "\t".$a.":'".$b."'";
$bFirstAtt = false;
} else {
echo ",\r\n\t".$a.":'".$b."'";
}
}
echo "}";
}
echo "\r\n};\r\n";
} catch( Exception $e ) {
echo "Exception on line ".$e->getLine()." of file ".$e->getFile()." : ".$e->getMessage()."<br/>";
}
To produce a JSON structure like this
var customers = {
'69236':{
editable:'true',
maxChars:'9',
valueType:'numeric'},
'12345':{
editable:'true',
maxChars:'9',
valueType:'numeric'},
'67890':{
editable:'true',
maxChars:'9',
valueType:'numeric'}
};
Finally, in your script, access the attribute like this
WScript.Echo( customers["12345"].editable );
Good luck
Related
Given the php code:
$xml = <<<EOF
<articles>
<article>
This is a link
<link>Title</link>
with some text following it.
</article>
</articles>
EOF;
function traverse($xml) {
$result = "";
foreach($xml->children() as $x) {
if ($x->count()) {
$result .= traverse($x);
}
else {
$result .= $x;
}
}
return $result;
}
$parser = new SimpleXMLElement($xml);
traverse($parser);
I expected the function traverse() to return:
This is a link Title with some text following it.
However, it returns only:
Title
Is there a way to get the expected result using simpleXML (obviously for the purpose of consuming the data rather than just returning it as in this simple example)?
There might be ways to achieve what you want using only SimpleXML, but in this case, the simplest way to do it is to use DOM. The good news is if you're already using SimpleXML, you don't have to change anything as DOM and SimpleXML are basically interchangeable:
// either
$articles = simplexml_load_string($xml);
echo dom_import_simplexml($articles)->textContent;
// or
$dom = new DOMDocument;
$dom->loadXML($xml);
echo $dom->documentElement->textContent;
Assuming your task is to iterate over each <article/> and get its content, your code will look like
$articles = simplexml_load_string($xml);
foreach ($articles->article as $article)
{
$articleText = dom_import_simplexml($article)->textContent;
}
node->asXML();// It's the simple solution i think !!
So, the simple answer to my question was: Simplexml can't process this kind of XML. Use DomDocument instead.
This example shows how to traverse the entire XML. It seems that DomDocument will work with any XML whereas SimpleXML requires the XML to be simple.
function attrs($list) {
$result = "";
foreach ($list as $attr) {
$result .= " $attr->name='$attr->value'";
}
return $result;
}
function parseTree($xml) {
$result = "";
foreach ($xml->childNodes AS $item) {
if ($item->nodeType == 1) {
$result .= "<$item->nodeName" . attrs($item->attributes) . ">" . parseTree($item) . "</$item->nodeName>";
}
else {
$result .= $item->nodeValue;
}
}
return $result;
}
$xmlDoc = new DOMDocument();
$xmlDoc->loadXML($xml);
print parseTree($xmlDoc->documentElement);
You could also load the xml using simpleXML and then convert it to DOM using dom_import_simplexml() as Josh said. This would be useful, if you are using simpleXml to filter nodes for parsing, e.g. using XPath.
However, I don't actually use simpleXML, so for me that would be taking the long way around.
$simpleXml = new SimpleXMLElement($xml);
$xmlDom = dom_import_simplexml($simpleXml);
print parseTree($xmlDom);
Thank you for all the help!
You can get the text node of a DOM element with simplexml just by treating it like a string:
foreach($xml->children() as $x) {
$result .= "$x"
However, this prints out:
This is a link
with some text following it.
TitleTitle
..because the text node is treated as one block and there is no way to tell where the child fits in inside the text node. The child node is also added twice because of the other else {}, but you can just take that out.
Sorry if I didn't help much, but I don't think there's any way to find out where the child node fits in the text node unless the xml is consistent (but then, why not use tags). If you know what element you want to strip the text out of, strip_tags() will work great.
This has already been answered, but CASTING TO STRING ( i.e. $sString = (string) oSimpleXMLNode->TagName) always worked for me.
Try this:
$parser = new SimpleXMLElement($xml);
echo html_entity_decode(strip_tags($parser->asXML()));
That's pretty much equivalent to:
$parser = simplexml_load_string($xml);
echo dom_import_simplexml($parser)->textContent;
Like #tandu said, it's not possible, but if you can modify your XML, this will work:
$xml = <<<EOF
<articles>
<article>
This is a link
</article>
<link>Title</link>
<article>
with some text following it.
</article>
</articles>
I am currently working on a project that requires me to query an XML file like php to return a value that matches the request. Take a look at the XML:
<ENVELOPE>
<MASTER>
<STKDETNAME>004-011</STKDETNAME>
<STKPNO>PTN771</STKPNO>
<STKPRICE></STKPRICE>
<STKOPBAL>500</STKOPBAL>
</MASTER>
<MASTER>
<STKDETNAME>004-012</STKDETNAME>
<STKPNO>PTN772</STKPNO>
<STKPRICE></STKPRICE>
<STKOPBAL>500</STKOPBAL>
</MASTER>
<MASTER>
<STKDETNAME>004-013</STKDETNAME>
<STKPNO>PTN773</STKPNO>
<STKPRICE></STKPRICE>
<STKOPBAL>1000</STKOPBAL>
</MASTER>
<MASTER>
<STKDETNAME>004-014</STKDETNAME>
<STKPNO>PTN774</STKPNO>
<STKPRICE></STKPRICE>
<STKOPBAL>1000</STKOPBAL>
</MASTER>
<MASTER>
<STKDETNAME>004-015</STKDETNAME>
<STKPNO>PTN775</STKPNO>
<STKPRICE>400</STKPRICE>
<STKOPBAL>1000</STKOPBAL>
</MASTER>
</ENVELOPE>
Now, I want to get the STKPRICE AND STKOPBAL for a SKTPNO= PTN773. This is what i have seen so far, but i don't know how to get the two values. I am new to XML.
$file = 'stocksum.xml';//same file as above
$xmlfile = simplexml_load_file($file);
$partno = PTN775;
$fnd = $xmlfile->xpath('/ENVELOPE/MASTER/STKPNO[.="$partno"]');
There are a couple of issues with the code which are just syntax problems, these are the partno needing quotes and when building the XPath expression, you use single quotes so it doesn't insert the actual part number.
BUT to get to your actual problem, if you change your XPath to the one used here, this will find the <MASTER> element whose <STKPNO> is the one your after. So then you can refer to the elements withing the <MASTER> element using standard SimpleXML object notation...
$partno = 'PTN775';
$fnd = $xmlfile->xpath('/ENVELOPE/MASTER[STKPNO="'.$partno.'"]');
echo $fnd[0]->STKPRICE.PHP_EOL;
Note that as xpath() returns a list of matches, I use $fnd[0] to get the first one.
Code which also has a check to see if the part actually exists...
$xmlfile = simplexml_load_file($file);
$partno = 'PTN7751';
$fnd = $xmlfile->xpath('/ENVELOPE/MASTER[STKPNO="'.$partno.'"]');
if ( count($fnd) == 0 ) {
echo "Not found";
}
else {
echo $fnd[0]->STKPRICE.PHP_EOL;
}
I'm trying to convert xml to json back to xml for testing a service and I'm having an issue w/ repeated keys being represented incorrectly.
The following valid XML is the starting point:
<foo>
<bars>
<bar>
<url>http://url</url>
</bar>
<bar>
<url>http://url</url>
</bar>
</bars>
</foo>
Which converts to json:
{"bars":{"bar":[{"url":"http:\/\/url"},{"url":"http:\/\/url"}]}}
Every solution I've seen to similar questions ends up rendering the resulting xml as something like:
<bars>
<bar>
<n0>
<url>http://url</url>
</n0>
<n1>
<url>http://url</url>
</n1>
</bar>
</bars>
Obviously, I need to get back to the original xml. And the structure is quite complex and variable, so I can't count on a particular structure.
Any ideas?
I've done a few functions which encode and decode XML, the first takes an XML source as a SimpleXMLElement and converts it into an array (note that it doesn't deal with attributes) but seems to work for your test case and a few I've tried (the example has a slight modification to the XML to check). The second takes the same array and converts it into a string with the XML reconstructed. There is a lot of recursion going on but the routines are quite short so hopefully easy(ish) to follow...
function xmlToArray ( $base, SimpleXMLElement $node ) {
$nodeName = $node->getName();
$childNodes = $node->children();
if ( count($childNodes) == 0 ) {
$base[ $nodeName ] = (string)$node;
}
else {
$new = [];
foreach ( $childNodes as $newNode ) {
$new[] = xmlToArray($base, $newNode);
}
$base[$nodeName] = count($new)>1?$new:$new[0];
}
return $base;
}
function arrayToXML ( $base ) {
foreach ( $base as $name => $node ) {
$xml = "<{$name}>";
if ( $node instanceof stdClass ){
$xml .= arrayToXML($node);
}
elseif ( is_array($node) ) {
foreach ( $node as $ele ){
$xml .= arrayToXML($ele);
}
}
else {
$xml .= $node;
}
$xml .= "</{$name}>";
}
return $xml;
}
$xml_string = <<< XML
<foo>
<bars>
<bar>
<url>http://url1</url>
</bar>
<bar>
<url>http://url2</url>
</bar>
<url>http://url3</url>
</bars>
</foo>
XML;ToXML ($dec);
echo $target;
ML ($dec);
echo $target;
$source = simplexml_load_string($xml_string);
$xml = xmlToArray([], $source);
$enc = json_encode($xml);
echo $enc.PHP_EOL;
$dec = json_decode($enc);
$target = arrayToXML ($dec);
echo $target;
This outputs the JSON and the XML at the end as...
{"foo":{"bars":[{"bar":{"url":"http:\/\/url1"}},{"bar":{"url":"http:\/\/url2"}},{"url":"http:\/\/url3"}]}}
<foo><bars><bar><url>http://url1</url></bar><bar><url>http://url2</url></bar><url>http://url3</url></bars></foo>
You may use php file handling function and read xml file line by line or number of characters for fixed length tag name and using simple if conditions, print json string on a file.
This may work out.
There are many different ways of converting JSON to XML, or XML to JSON. They all work differently, and there is no single method that is always best. They all have to make some kind of compromise between usability and faithful round-tripping (for example, your library has dropped the outer "foo" element, which therefore can't be reconstituted on the reverse conversion).
You could devise a mapping of arbitrary XML to JSON that allows faithful round-tripping back to XML, but the JSON representation wouldn't be particularly user-friendly, especially for example if you need faithful round-tripping of namespaces.
XSLT 3.0 incidentally does the reverse: it has functions that will convert any JSON input losslessly to (a rather unfriendly vocabulary of) XML, and then convert the result faithfully back to the original JSON. You need the opposite of that.
I have a page in php where I have to parse an xml.
I have done this for example:
$hotelNodes = $xml_data->getElementsByTagName('Hotel');
foreach($hotelNodes as $hotel){
$supplementsNodes2 = $hotel->getElementsByTagName('BoardBase');
foreach($supplementsNodes2 as $suppl2) {
echo'<p>HERE</p>'; //not enter here
}
}
}
In this code I access to each hotel of my xml, and foreach hotel I would like to search the tag BoardBase but it doesn0t enter inside it.
This is my xml (cutted of many parts!!!!!)
<hotel desc="DESC" name="Hotel">
<selctedsupplements>
<boardbases>
<boardbase bbpublishprice="0" bbprice="0" bbname="Colazione Continentale" bbid="1"></boardbase>
</boardbases>
</selctedsupplements>
</occupancy></occupancies>
</hotel>
I have many nodes that doesn't have BoardBase but sometimes there is but not enter.
Is possible that this node isn't accessible?
This xml is received by a server with a SoapClient.
If I inspect the XML printed in firebug I can see the node with opacity like this:
I have also tried this:
$supplementsNodes2 = $hotel->getElementsByTagName('boardbase');
but without success
2 issues I can see from the get-go: XML names are case-sensitive, hence:
$hotelNodes = $xml_data->getElementsByTagName('Hotel');
Can't work, because your xml node looks like:
<hotel desc="DESC" name="Hotel">
hotel => lower-case!
As you can see here:
[...] names for such things as elements, while XML is explicitly case sensitive.
The official specs specify tag names as case-sensitive, so getElementsByTagName('FOO') won't return the same elements as getElementsByTagName('foo')...
Secondly, you seem to have some tag-soup going on:
</occupancy></occupancies>
<!-- tag names don't match, both are closing tags -->
This is just plain invalid markup, it should read either:
<occupancy></occupancy>
or
<occupancies></occupancies>
That would be the first 2 ports of call.
I've set up a quick codepad using this code, which you can see here:
$xml = '<hotel desc="DESC" name="Hotel">
<selctedsupplements>
<boardbases>
<boardbase bbpublishprice="0" bbprice="0" bbname="Colazione Continentale" bbid="1"></boardbase>
</boardbases>
</selctedsupplements>
<occupancy></occupancy>
</hotel>';
$dom = new DOMDocument;
$dom->loadXML($xml);
$badList = $dom->getElementsByTagName('Hotel');
$correctList = $dom->getElementsByTagName('hotel');
echo sprintf("%d",$badList->lenght),
' compared to ',
$correctList->length, PHP_EOL;
The output was "0 compared to 1", meaning that using a lower-case selector returned 1 element, the one with the upper-case H returned an empty list.
To get to the boardbase tags for each hotel tag, you just have to write this:
$hotels = $dom->getElementsByTagName('html');
foreach($hotels as $hotel)
{
$supplementsNodes2 = $hotel->getElementsByTagName('boardbase');
foreach($supplementsNodes2 as $node)
{
var_dump($node);//you _will_ get here now
}
}
As you can see on this updated codepad.
Alessandro, your XML is a mess (=un casino), you really need to get that straight. Elias' answer pointed out some very basic stuff to consider.
I built on the code pad Elias has been setting up, it is working perfectly with me:
$dom = new DOMDocument;
$dom->loadXML($xml);
$hotels = $dom->getElementsByTagName('hotel');
foreach ($hotels as $hotel) {
$bbs = $hotel->getElementsByTagName('boardbase');
foreach ($bbs as $bb) echo $bb->getAttribute('bbname');
}
see http://codepad.org/I6oxkEOC
Given the php code:
$xml = <<<EOF
<articles>
<article>
This is a link
<link>Title</link>
with some text following it.
</article>
</articles>
EOF;
function traverse($xml) {
$result = "";
foreach($xml->children() as $x) {
if ($x->count()) {
$result .= traverse($x);
}
else {
$result .= $x;
}
}
return $result;
}
$parser = new SimpleXMLElement($xml);
traverse($parser);
I expected the function traverse() to return:
This is a link Title with some text following it.
However, it returns only:
Title
Is there a way to get the expected result using simpleXML (obviously for the purpose of consuming the data rather than just returning it as in this simple example)?
There might be ways to achieve what you want using only SimpleXML, but in this case, the simplest way to do it is to use DOM. The good news is if you're already using SimpleXML, you don't have to change anything as DOM and SimpleXML are basically interchangeable:
// either
$articles = simplexml_load_string($xml);
echo dom_import_simplexml($articles)->textContent;
// or
$dom = new DOMDocument;
$dom->loadXML($xml);
echo $dom->documentElement->textContent;
Assuming your task is to iterate over each <article/> and get its content, your code will look like
$articles = simplexml_load_string($xml);
foreach ($articles->article as $article)
{
$articleText = dom_import_simplexml($article)->textContent;
}
node->asXML();// It's the simple solution i think !!
So, the simple answer to my question was: Simplexml can't process this kind of XML. Use DomDocument instead.
This example shows how to traverse the entire XML. It seems that DomDocument will work with any XML whereas SimpleXML requires the XML to be simple.
function attrs($list) {
$result = "";
foreach ($list as $attr) {
$result .= " $attr->name='$attr->value'";
}
return $result;
}
function parseTree($xml) {
$result = "";
foreach ($xml->childNodes AS $item) {
if ($item->nodeType == 1) {
$result .= "<$item->nodeName" . attrs($item->attributes) . ">" . parseTree($item) . "</$item->nodeName>";
}
else {
$result .= $item->nodeValue;
}
}
return $result;
}
$xmlDoc = new DOMDocument();
$xmlDoc->loadXML($xml);
print parseTree($xmlDoc->documentElement);
You could also load the xml using simpleXML and then convert it to DOM using dom_import_simplexml() as Josh said. This would be useful, if you are using simpleXml to filter nodes for parsing, e.g. using XPath.
However, I don't actually use simpleXML, so for me that would be taking the long way around.
$simpleXml = new SimpleXMLElement($xml);
$xmlDom = dom_import_simplexml($simpleXml);
print parseTree($xmlDom);
Thank you for all the help!
You can get the text node of a DOM element with simplexml just by treating it like a string:
foreach($xml->children() as $x) {
$result .= "$x"
However, this prints out:
This is a link
with some text following it.
TitleTitle
..because the text node is treated as one block and there is no way to tell where the child fits in inside the text node. The child node is also added twice because of the other else {}, but you can just take that out.
Sorry if I didn't help much, but I don't think there's any way to find out where the child node fits in the text node unless the xml is consistent (but then, why not use tags). If you know what element you want to strip the text out of, strip_tags() will work great.
This has already been answered, but CASTING TO STRING ( i.e. $sString = (string) oSimpleXMLNode->TagName) always worked for me.
Try this:
$parser = new SimpleXMLElement($xml);
echo html_entity_decode(strip_tags($parser->asXML()));
That's pretty much equivalent to:
$parser = simplexml_load_string($xml);
echo dom_import_simplexml($parser)->textContent;
Like #tandu said, it's not possible, but if you can modify your XML, this will work:
$xml = <<<EOF
<articles>
<article>
This is a link
</article>
<link>Title</link>
<article>
with some text following it.
</article>
</articles>