In answering a previous question I found the following behaviour which I can't understand. The following code shows the issue...
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
$data = <<< XML
<?xml version="1.0" standalone="yes"?>
<Base>
<Data>
<Value></Value>
</Data>
</Base>
XML;
$xml = simplexml_load_string($data);
foreach ( $xml->Data->Value as $value ) {
$value = 1;
}
echo $xml->asXML().PHP_EOL;
foreach ( $xml->Data as $value ) {
$value->Value = 1;
}
echo $xml->asXML().PHP_EOL;
I would expect the output at each point to be the same, but the output is...
<?xml version="1.0" standalone="yes"?>
<Base>
<Data>
<Value/>
</Data>
</Base>
<?xml version="1.0" standalone="yes"?>
<Base>
<Data>
<Value>1</Value>
</Data>
</Base>
So this seems to indicate that the first loop which directly accesses the <Value> element, doesn't set the value and yet the second loop which indirectly accesses it works OK.
What is the difference?
The difference is nothing to do with the loops, or with references, but with what exactly = means in each case.
The first version can be simplified to this:
$value = $xml->Data->Value;
$value = 1;
This is a straight-forward assignment to a variable, first of one value, and then of another. There's no interaction between the old value and the new one, so $xml is not changed.
The second case can be written like this:
$data = $xml->Data;
$data->Value = 1;
// Or just $xml->Data->Value = 1;
Here, we are assigning not to a normal variable, but to an object property, and the trick is that the object can intercept that assignment, and do something special with it. In this case, it triggers SimpleXML to send the value to the libxml representation of the XML document in memory. It is as though you had run a method call like $data->setValueOfChild('Value', 1);.
Note that if we instead wrote this:
$value =& $xml->Data->Value;
$value = 1;
Now the first assignment sets $value to be a reference, and the second assigns 1 to that reference. This is enough to write the value to an actual object property, but does not trigger the interception SimpleXML needs.
However, there is one additional trick we can use in this particular case: as well as intercepting property access, the SimpleXMLElement class intercepts array access so that you can write $foo->NameThatOccursMoreThanOnce[3] and $some_element['Attribute']. So it turns out we can write this:
$value = $xml->Data->Value;
$value[0] = 1;
Here, $value is a SimpleXMLElement object, which can intercept the $value[0] = 1 as something like $value->setValueOfItem(0, 1).
In this case, the object holds the collection of all elements called <Value> from inside the <Data> element; but conveniently, even if the object has already been narrowed down to one item, [0] just refers back to the same element, so this works too:
$value = $xml->Data->Value[0];
$value[0] = 1;
Finally, a quick note that your own objects can implement this magic behaviour too! The property access can be implemented using the __get, __set, and __unset magic methods, and the array access can be implemented using the ArrayAccess interface.
Related
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.
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 am trying to build an XML element from an array in PHP using an example that was posted elsewhere on this site. The XML string is being created as expected however the nodes and their values are reversed. For Example:
$params = array(
"email" => 'me#gmail.com'
,"pass" => '123456'
,"pass-conf" => '123456'
);
$xml = new SimpleXMLElement('<root/>');
array_walk_recursive($params, array($xml, 'addChild'));
echo $xml->asXML();
Now what I am expecting to be returned is:
<?xml version="1.0"?>
<root>
<email>me#gmail.com</email>
<pass>123456</pass>
<pass-conf>123456</pass-conf>
</root>
However, what I keep getting is the node names as values and values as node names:
<?xml version="1.0"?>
<root>
<me#gmail.com>email</me#gmail.com>
<123456>pass</123456>
<123456>pass-conf</123456>
</root>
I have tested switching the key with the value in the $params array, but that seems like a lazy hack to me. I believe the issue lies within my callback in array_walk_recursive, but I'm not sure how exactly to make it work. I am open to recommendations on better ways to convert a PHP array to XML. I just tried this because it seemed simple and not convoluted. (haha..)
The problem with your code is that array_walk_recursive supplies the callback with the arguments value then key (in that order).
SimpleXMLElement::addChild accepts the arguments name then value (in that order).
Here's a less convoluted solution
foreach ($params as $key => $value) {
$xml->addChild($key, $value);
}
https://3v4l.org/oOHSb
I have xml with the following structure:
<?xml version="1.0"?>
<ONIXMessage xmlns="http://test.com/test">
...data...
</ONIXMessage>
I need to change xmlns attribute with my own value. How can I do it? Preferably with DOMDocument class.
I need to change xmlns attribute with my own value. How can I do it? Preferably with DOMDocument class.
This by design is not possible. Every DOMDocument has a single root/document element.
In your example XML that root element is:
{http://test.com/test}ONIXMessage
I write the element name as an expanded-name with the convention to put the namespace URI in front enclosed in angle brackets.
Writing the element name in a form that shows it's entire expanded-name also demonstrates that you do not only want to change the value of an attribute here, but you want to change the namespace URI of a specific element. So you want to change the element name. And probably also any child element name it contains if the child is in the same namespace.
As the xmlns attribute only reflects the namespace URI of the element itself, you can not change it. Once it is set in DOMDocument, you can not change it.
You can replace the whole element, but the namespace of the children is not changed either then. Here an example with an XML similar to yours with only textnode children (which aren't namespaced):
$xml = <<<EOD
<?xml version="1.0"?>
<ONIXMessage xmlns="uri:old">
...data...
</ONIXMessage>
EOD;
$doc = new DOMDocument();
$doc->loadXML($xml);
$newNode = $doc->createElementNS('uri:new', $doc->documentElement->tagName);
$oldNode = $doc->replaceChild($newNode, $doc->documentElement);
foreach(iterator_to_array($oldNode->childNodes, true) as $child) {
$doc->documentElement->appendChild($child);
}
Resulting XML output is:
<?xml version="1.0"?>
<ONIXMessage xmlns="uri:new">
...data...
</ONIXMessage>
Changing the input XML now to something that contains children like
<?xml version="1.0"?>
<ONIXMessage xmlns="uri:old">
<data>
...data...
</data>
</ONIXMessage>
Will then create the following output, take note of the old namespace URI that pops up now again:
<?xml version="1.0"?>
<ONIXMessage xmlns="uri:new">
<default:data xmlns:default="uri:old">
...data...
</default:data>
</ONIXMessage>
As you can see DOMDocument does not provide a functionality to replace namespace URIs for existing elements out of the box. But hopefully with the information provided in this answer so far it is more clear why exactly it is not possible to change that attributes value if it already exists.
The expat based parser in the libxml based PHP extension does allow to "change" existing attribute values regardless if it is an xmlns* attribute or not - because it just parses the data and you can process it on the fly with it.
A working example is:
$xml = <<<EOD
<?xml version="1.0" encoding="utf-8"?>
<ONIXMessage xmlns="uri:old">
<data>
...data...
</data>
</ONIXMessage>
EOD;
$uriReplace = [
'uri:old' => 'uri:new',
];
$parser = xml_parser_create('UTF-8');
xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
xml_set_default_handler($parser, function ($parser, $data) {
echo $data;
});
xml_set_element_handler($parser, function ($parser, $name, $attribs) use ($xml, $uriReplace) {
$selfClosing = '/>' === substr($xml, xml_get_current_byte_index($parser), 2);
echo '<', $name;
foreach ($attribs as $name => $value) {
if (substr($name, 0, 5) === 'xmlns' && isset($uriReplace[$value])) {
$value = $uriReplace[$value];
}
printf(' %s="%s"', $name, htmlspecialchars($value, ENT_COMPAT | ENT_XML1));
}
echo $selfClosing ? '/>' : '>';
}, function ($parser, $name) use ($xml) {
$selfClosing = '/>' === substr($xml, xml_get_current_byte_index($parser) - 2, 2);
if ($selfClosing) return;
echo '</', $name, '>';
});
xml_parse($parser, $xml, true);
xml_parser_free($parser);
The output then has transparently changed the namespace URI from uri:old to uri:new:
<ONIXMessage xmlns="uri:new">
<data>
...data...
</data>
</ONIXMessage>
As this example shows, each XML feature you make use of in your XML needs to be handled with the parser. For example the XML declaration is missing. However these can be added by implementing missing handler classbacks (e.g. for CDATA sections) or by outputting missing output (e.g. for the "missing" XML declaration). I hope this is helpful and shows you an alternative way on how to change even these values that are not intended to change.
I am having trouble getting the text "TestTwo" where the parent has an attribute with Name=SN from the XML below. I am able to get the SimpleXMLElement, but cannot figure out how to get the child.
What is the best way to return the text "TestTwo"?
Thank you!
$xml = simplexml_load_string($XMLBELOW);
$xml->registerXPathNamespace('s','urn:oasis:names:tc:SAML:2.0:assertion');
$result = $xml->xpath("//s:Attribute[#Name='sn']");
var_dump( $result);
$XMLBELOW = <<<XML
<?xml version="1.0" ?>
<saml2:Assertion ID="SAML-4324423" IssueInstant="2012-09-24T17:49:39Z" Version="2.0" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
<saml2:Issuer>
Test
</saml2:Issuer>
<saml2:Subject>
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" NameQualifier="Test">
Tester
</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml2:SubjectConfirmationData NotBefore="2012-09-24T17:48:39Z" NotOnOrAfter="2012-09-24T17:51:39Z"/>
</saml2:SubjectConfirmation>
</saml2:Subject>
<saml2:Conditions NotBefore="2012-09-24T17:48:39Z" NotOnOrAfter="2012-09-24T17:51:39Z"/>
<saml2:AuthnStatement AuthnInstant="2012-09-24T17:49:39Z" SessionNotOnOrAfter="2012-09-24T17:51:39Z">
<saml2:SubjectLocality Address="105.57.487.48"/>
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified
</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
<saml2:AttributeStatement>
<saml2:Attribute Name="sn" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml2:AttributeValue>
TestTwo
</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute Name="departmentNumber" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml2:AttributeValue>
OPERS
</saml2:AttributeValue>
</saml2:Attribute>
</saml2:AttributeStatement>
</saml2:Assertion>
XML;
How about this?
$result = $xml->xpath("//s:Attribute[#Name='sn']/*");
var_dump( $result[0]->__toString() ); // or just `echo $result[0];`
The updated expression will get you all children of your target element (you can narrow the result set further, if you'd like).
As xpath() method returns an array, you need to take some of its elements, and just call 'toString' on these objects to get its text. Here I've done with the first one.