I'm not to experienced with XML and I have been trying to figure this out without to much progress.
I need to get the results from the XML file to a array using PHP.
Here is the XML
<ns2:messageContainer xmlns="datex2.eu/schema/3/common" xmlns:ns2="datex2.eu/schema/3/messageContainer" xmlns:ns3="datex2.eu/schema/3/exchangeInformation" xmlns:ns4="datex2.eu/schema/3/informationManagement" xmlns:ns5="datex2.eu/schema/3/dataDictionaryExtension" xmlns:ns6="datex2.eu/schema/3/cctvExtension" xmlns:ns7="datex2.eu/schema/3/locationReferencing" xmlns:ns8="datex2.eu/schema/3/alertCLocationCodeTableExtension" xmlns:ns9="datex2.eu/schema/3/extension" xmlns:ns10="datex2.eu/schema/3/roadTrafficData" xmlns:ns11="datex2.eu/schema/3/vms" xmlns:ns12="datex2.eu/schema/3/situation" modelBaseVersion="3">
<ns2:payload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ns7:PredefinedLocationsPublication" lang="no" modelBaseVersion="3">
<publicationTime>2023-01-25T13:56:15.615+01:00</publicationTime>
<publicationCreator>
<country>no</country>
<nationalIdentifier>NPRA</nationalIdentifier>
</publicationCreator>
<ns7:headerInformation>
<confidentiality>noRestriction</confidentiality>
<informationStatus>real</informationStatus>
</ns7:headerInformation>
<ns7:predefinedLocationReference xsi:type="ns7:PredefinedLocation" id="100356" version="1">
<ns7:predefinedLocationName>
<values>
<value lang="no">Eikås - Åsanevegen</value>
</values>
</ns7:predefinedLocationName>
<ns7:location xsi:type="ns7:LinearLocation">
<ns7:gmlLineString srsName="http://www.opengis.net/gml/srs/epsg.xml#32633">
<ns7:posList>-27946 6743813</ns7:posList>
</ns7:gmlLineString>
</ns7:location>
</ns7:predefinedLocationReference>
<ns7:predefinedLocationReference xsi:type="ns7:PredefinedLocation" id="100361" version="1">
<ns7:predefinedLocationName>
<values>
<value lang="no">Ammerud - Bjerke</value>
</values>
</ns7:predefinedLocationName>
<ns7:location xsi:type="ns7:LinearLocation">
<ns7:gmlLineString srsName="http://www.opengis.net/gml/srs/epsg.xml#32633">
<ns7:posList>269553 6653843</ns7:posList>
</ns7:gmlLineString>
</ns7:location>
</ns7:predefinedLocationReference>
</ns2:payload>
<ns2:exchangeInformation modelBaseVersion="3">
<ns3:exchangeContext>
<ns3:codedExchangeProtocol>snapshotPull</ns3:codedExchangeProtocol>
<ns3:exchangeSpecificationVersion>3</ns3:exchangeSpecificationVersion>
<ns3:supplierOrCisRequester>
<ns3:internationalIdentifier>
<country>no</country>
<nationalIdentifier>NPRA</nationalIdentifier>
</ns3:internationalIdentifier>
</ns3:supplierOrCisRequester>
</ns3:exchangeContext>
<ns3:dynamicInformation>
<ns3:exchangeStatus>undefined</ns3:exchangeStatus>
<ns3:messageGenerationTimestamp>2023-01-25T13:56:15.615+01:00</ns3:messageGenerationTimestamp>
</ns3:dynamicInformation>
</ns2:exchangeInformation>
</ns2:messageContainer>
Here is my PHP code
$xml = simplexml_load_string($response->raw_body, "SimpleXMLElement", LIBXML_NOCDATA, 'ns2', true);
$xml->registerXPathNamespace('ns7','http://datex2.eu/schema/3/locationReferencing');
$count = 0;
foreach($xml->xpath('//ns7:predefinedLocationReference') as $event) {
$return[$count]['id'] = intval($event->attributes()->id);
$predefinedLocationName = $event->xpath('ns7:predefinedLocationName');
foreach ($predefinedLocationName[0]->values as $locVal) {
$return[$count]['name'] = strval($locVal->value);
}
$count++;
}
I'm sure there is a better way but here is what I got:
{
"id": 100356,
"name": "Eikås - Åsanevegen"
},
{
"id": 100361,
"name": "Ammerud - Bjerke"
}
What I'm missing is to get out the posList value from the XML and add it to my array in PHP
Rather than messing around with XPath, I would use the main SimpleXML access methods, noting this reference of how SimpleXML handles namespaces.
Specifically, I would note down the path I wanted to take through the document, expanding namespaces to their full identifiers rather than their local aliases:
Start at messageContainer, in the datex2.eu/schema/3/messageContainer namespace
Go into payload, in the same namespace
Loop over each predefinedLocationReference, in the datex2.eu/schema/3/locationReferencing namespace
Get the "id" from the (non-namespaced) id attribute
Go into predefinedLocationName, still in the datex2.eu/schema/3/locationReferencing namespace
Go into values, in the datex2.eu/schema/3/common namespace (defined as the default xmlns at the top of the document)
Get the "name" from the value element in that namespace
From the predefinedLocationReference we had earlier, go into the location (in the same namespace as predefinedLocationReference)
Go into gmlLineString, in the same namespace
Get the "postList" from the posList, in the same namespace
That then translates directly to this PHP code:
// Some constants to make namespaces easier to read
const NS_MSG_CONT = 'datex2.eu/schema/3/messageContainer';
const NS_LOC_REF = 'datex2.eu/schema/3/locationReferencing';
const NS_COMMON = 'datex2.eu/schema/3/common';
// Initialise our return array
$return = [];
// Start at `messageContainer`, in the `datex2.eu/schema/3/messageContainer` namespace
$xml = simplexml_load_string($response->raw_body, "SimpleXMLElement", 0, NS_MSG_CONT);
// Go into `payload`, in the same namespace
$payload = $xml->payload;
// Loop over each `predefinedLocationReference`, in the `datex2.eu/schema/3/locationReferencing` namespace
foreach ($payload->children(NS_LOC_REF)->predefinedLocationReference as $predefinedLocationReference ) {
// Initialise the return item
$item = [];
// Get the "id" from the (non-namespaced) `id` attribute
$item['id'] = (string)$predefinedLocationReference->attributes(null)->id;
// Go into `predefinedLocationName`, still in the `datex2.eu/schema/3/locationReferencing` namespace
$predefinedLocationName = $predefinedLocationReference->predefinedLocationName;
// Go into `values`, in the `datex2.eu/schema/3/common` namespace (defined as the default `xmlns` at the top of the document)
$values = $predefinedLocationName->children(NS_COMMON)->values;
// Get the "name" from the `value` element in that namespace
$item['name'] = (string)$values->value;
// From the `predefinedLocationReference` we had earlier, go into the `location` (in the same namespace as `predefinedLocationReference`)
$location = $predefinedLocationReference->location;
// Go into `gmlLineString`, in the same namespace
$gmlLineString = $location->gmlLineString;
// Get the "posList" from the `posList`, in the same namespace
$item['posList'] = (string)$gmlLineString->posList;
// Add item to our final results
$return[] = $item;
}
// Test
var_dump($return);
This can obviously be made much shorter by removing comments and intermediate variables to taste; a very shortened version of exactly the same code looks like this:
const NS_MSG_CONT = 'datex2.eu/schema/3/messageContainer';
const NS_LOC_REF = 'datex2.eu/schema/3/locationReferencing';
const NS_COMMON = 'datex2.eu/schema/3/common';
$xml = simplexml_load_string($raw_body, "SimpleXMLElement", 0, NS_MSG_CONT);
// Note: in PHP >8.0, you can skip the parameters you're not interested in:
// $xml = simplexml_load_string($raw_body, namespace_or_prefix: NS_MSG_CONT);
$return = [];
foreach ($xml->payload->children(NS_LOC_REF)->predefinedLocationReference as $predefinedLocationReference ) {
$return[] = [
'id' => (string)$predefinedLocationReference->attributes(null)->id,
'name' => (string)$predefinedLocationReference->predefinedLocationName->children(NS_COMMON)->values->value,
'posList' => (string)$predefinedLocationReference->location->gmlLineString->posList,
];
}
Related
During an export of my data, I can choose between CSV and JSON format.
Viewing the CSV, data, including column names are arranged in the exact same way as I would like to import into MySQL database.
CSV screenshot
I'm getting JSON from a webpage and it's stored in $data variable.
<html>
<body>
<?php
// Include Parsehub REST api wrapper
require_once __DIR__ . '/vendor/autoload.php';
use Parsehub\Parsehub;
$api_key = "XXX";
$project_token = "XXX";
$parsehub = new Parsehub($api_key);
$data = $parsehub->getLastReadyRunData($project_token);
echo $data;
?>
</body>
</html>
Echo output of it would return
JSON:
https://pastebin.com/raw/AZt4gvsC
CREATE TABLE:
CREATE TABLE `utakmice_1` (
`utakmica_name` varchar(64) NOT NULL,
`utakmica_url` varchar(256) NOT NULL,
`utakmica_liga` varchar(64) NOT NULL,
`utakmica_liga_url` varchar(256) NOT NULL,
`utakmica_vreme` varchar(64) NOT NULL,
`utakmica_datum` varchar(64) NOT NULL,
`utakmica_kvote_kvota` decimal(10,2) NOT NULL,
`utakmica_kvote_kladionica` varchar(63) NOT NULL,
`utakmica_kvote_kladionica_url` varchar(256) NOT NULL,
`utakmica_kvote_igra` varchar(1) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
In which way could all the json data could be inserted?
Using PHP, you need to:
Convert the JSON data to an associative array using json_decode
$data = json_decode($jsondata, true);
Process that array to get the data you need to insert in the table
$name = $data['utakmica']['name'];
$url = $data['utakmica']['age'];
etc
Insert to MySQL using the Insert query (we're using mysqli here but you could use PDO)
$sql = "INSERT INTO utakmice_1(utakmica_name, utakmica_url)
VALUES('$name', '$url')";
if(!mysqli_query($con, $sql))
{
die('Error : ' . mysql_error());
}
Given the CSV display and the structure in the JSON, when doing a standard recursive iteration over the JSON structure with the modifcation to terminate leafs being objects that no longer contain a property that is an array, it would be possible going over all leaf nodes having their property names prefixed with all parent property names (dropping array indexes) and using all lower depth current objects (from the subiterators) in depth ascending order to create a merged object to obtain such objects with such keys.
The boilerplate of this is RecursiveArrayIterator and RecursiveIteratorIterator.
The important part is that if an object has a property that is an array, it is the only property that is an array and the array is an array of objects of which are all homogeneous. This is the case for your JSON Text, and this is important as both the RecursiveArrayIterator and the RecursiveIteratorIterator need to be modified for such traversal.
The unmodified boilerplate behaviour is also shown in the related Q&A How can I parse a JSON file with PHP?:
$it = new RecursiveIteratorIterator(
new RecursiveArrayIterator($json)
);
foreach ($it as $property => $value) {
...
}
As in your use-case both objects need to be modified and it only works with leaves only iteration (as only then merging the objects makes sense), the whole construct could be extended from RecursiveIteratorIterator, overwriting the constructor taking the $json directly, using the modification of RecursiveArrayIterator.
Speaking of it, it needs to keep the key-prefix (here implemented as a private property), turn objects into non-traversable children (standard behaviour is every non-object and non-array property construes a leaf) and removal of array properties from each node as those should only be available to traverse up to leaf nodes and then when merging across the whole path in depth ascending order, they would be superfluous:
$it = new class ($json) extends \RecursiveIteratorIterator {
public function __construct(object $json)
{
$jsonObjectRecursion = new class($json) extends \RecursiveArrayIterator
{
private ?string $keyPrefix = null;
public function key()
{
return (null !== $this->keyPrefix ? $this->keyPrefix . '_' : '') . parent::key();
}
public function current(): object|array
{
$current = parent::current();
if (is_array($current)) {
return $current;
}
$current = array_filter(get_object_vars($current), static fn ($any) => !is_array($any));
if (null !== $this->keyPrefix) {
$current = array_combine(preg_filter('/^/', "{$this->keyPrefix}_", array_keys($current)), $current);
}
return (object)$current;
}
public function hasChildren(): bool
{
$current = parent::current();
if (is_array($current)) {
return parent::hasChildren();
}
return (is_object($current) && array_filter(get_object_vars($current), 'is_array'));
}
public function getChildren(): self
{
$current = parent::current();
if (is_array($current)) {
$children = parent::getChildren();
$children->keyPrefix = $this->key();
return $children;
}
$probe = array_filter(get_object_vars($current), 'is_array');
$children = new self((object) [key($probe) => current($probe)]);
$children->keyPrefix = $this->keyPrefix;
return $children;
}
};
parent::__construct($jsonObjectRecursion);
}
...
As this excerpt shows, array traversal is preserved and the key-prefix is passed along accordingly. The array handling within current() can perhaps be dropped as arrays only come into action with has/getChildren() invocations.
The $json is just this one big JSON Object decoded into PHPs' stdClass. Despite it has Array in its name, the RecursiveArrayIterator can handle these JSON Objects well without converting them into an array.
Next to the constructor which merely only defines the inner iterator, this RecursiveIteratorITerator only implements one second method, current(), which does the merging:
...
public function current(): object
{
$object = [];
for ($depth = 0; $depth <= $this->getDepth(); $depth++) {
$current = $this->getSubIterator($depth)->current();
if (is_array($current)) {
continue;
}
$object += get_object_vars($current);
}
return (object)$object;
}
};
As $it is now defined, all that needs is to iterate over it and proceed with the values:
foreach ($it as $row) {
echo json_encode($row, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), "\n";
break;
}
Output (first flat row):
{
"utakmica_name": "Deportivo Kuenka-Un. Katolika Kito",
"utakmica_url": "https://www.fudbal91.com/soccer_clubs/compare/Deportivo_Cuenca_vs_Un._Catolica_Quito/13641",
"utakmica_liga": "Ekvador 1",
"utakmica_liga_url": "https://www.fudbal91.com/competition/Ecuador,_LigaPro_betcris_-_Primera_Etapa/2022",
"utakmica_vreme": "00:00",
"utakmica_datum": "< 22.03.2022. (Utorak) >",
"utakmica_kvote_kvota": "2.75",
"utakmica_kvote_kladionica": "BetOle",
"utakmica_kvote_kladionica_url": "https://example.com/2YjM4Ft",
"utakmica_kvote_igra": "1"
}
To turn $it into an array of objects, use iterator_to_array($it, false). E.g. when you're looking forward to sort the whole list by one attribute without always needing to traverse the whole tree again and again for every operation.
Just as-if you would have used the CSV export that is already flat.
Such and even many more tips can be found in the related Q&A How to extract and access data from JSON with PHP? and many more others on the site.
$data = "<QRYRESULT>
<ISSUCCESS>Y</ISSUCCESS>
<EBLCUSTOMER ACCOUNTNO='11111'>
<CUSTACCTNO>121212</CUSTACCTNO>
<ACCTSTATUS>active</ACCTSTATUS>
<CCYDESC>BDT</CCYDESC>
<BALANCE>9999</BALANCE>
<AVAILABLEBALANCE>99</AVAILABLEBALANCE>
<CUSTOMERNAME>cus_name</CUSTOMERNAME>
<AMOUNTONHOLD>1000</AMOUNTONHOLD>
<ODLIMIT>99</ODLIMIT>
</EBLCUSTOMER>
</QRYRESULT>";
this is the XML string I am trying to convert. I have used the folloung code.
$result = str_replace(array("\n", "\r", "\t"), '', $data);
$xml = simplexml_load_string($result);
$object = new stdclass();
$object->webservice[] = $xml;
$result = json_encode($object);
header('content-Type: application/json');
echo $result;
And I am getting the following json data.
{
"webservice": [
{
"ISSUCCESS": "Y",
"CUSTSUMMARY": {
"#attributes": {
"ACCOUNT": "11111"
},
"IDACCOUNT": "1010101",
"CODACCTCURR": "BDT",
"NUMBALANCE": "99999",
"ACCTDESC": "22222",
"PRDNAME": "name"
}
}
]
}
But i don't want the "#attributes". I want the output like below:
{
"QRYRESULT": {
"ISSUCCESS": "Y",
"EBLCUSTOMER": {
"-ACCOUNTNO": "11111",
"CUSTACCTNO": "121212",
"ACCTSTATUS": "active",
"CCYDESC": "BDT",
"BALANCE": "9999",
"AVAILABLEBALANCE": "99",
"CUSTOMERNAME": "cus_name",
"AMOUNTONHOLD": "1000",
"ODLIMIT": "99"
}
}
}
How can I do that ?
You don't want to have the "#attributes" field encoded in the JSON, however this is the standard way how PHP JSON serializes a SimpleXMLElement.
As you say you want to change that, you need to change the way how PHP JSON serializes the object. This is possible by implementing JsonSerializable with a SimpleXMLElement on your own and then provide the JSON serialization as you wish:
class JsonSerializer extends SimpleXmlElement implements JsonSerializable
{
/**
* SimpleXMLElement JSON serialization
*
* #return null|string
*
* #link http://php.net/JsonSerializable.jsonSerialize
* #see JsonSerializable::jsonSerialize
*/
function jsonSerialize()
{
// jishan's SimpleXMLElement JSON serialization ...
return $serialized;
}
}
E.g. by using the attributes as fields like all the child elements.
You can then just integrate it easily, e.g. instead of
$xml = simplexml_load_string($result);
you can use
$xml = simplexml_load_string($result, 'JsonSerializer');
or just
$xml = new JsonSerializer($result);
and the rest of your function works the same but just with your wishes serialization.
Example:
$result = str_replace(array("\n", "\r", "\t"), '', $data);
$xml = new JsonSerializer($result);
$object = new stdclass();
$object->webservice[] = $xml;
$result = json_encode($object, JSON_PRETTY_PRINT);
header('content-Type: application/json');
echo $result;
Output:
{
"webservice": [
{
"EBLCUSTOMER": {
"ACCOUNTNO": "11111",
"CUSTACCTNO": "121212",
"ACCTSTATUS": "active",
"CCYDESC": "BDT",
"BALANCE": "9999",
"AVAILABLEBALANCE": "99",
"CUSTOMERNAME": "cus_name",
"AMOUNTONHOLD": "1000",
"ODLIMIT": "99"
}
}
]
}
The serialization function for the example above is:
function jsonSerialize()
{
// text node (or mixed node represented as text or self closing tag)
if (!count($this)) {
return $this[0] == $this
? trim($this) : null ;
}
// process all child elements and their attributes
foreach ($this as $tag => $element) {
// attributes first
foreach ($element->attributes() as $name => $value) {
$array[$tag][$name] = $value;
}
// child elements second
foreach($element as $name => $value) {
$array[$tag][$name] = $value;
}
}
return $array;
}
Some notes here:
In the serialization you have to take care of the type of element your own. The differentiation is done on top for the single elements with no children. If you need attribute handling on these, you need to add it.
The trim($this) perhaps already spares you the issue you try to catch with $result = str_replace(array("\n", "\r", "\t"), '', $data);. SimpleXMLElement in any case would JSON serialize "\r" characters (SimpleXMLElement makes use of "\n" for breaks). Additionally you might be interested in the rules of whitespace normalization in XML.
In case an attribute has the same name as a child element, it will be overwritten by the child element.
In case a child element that follows another child element with the same name, it will be overwritten.
The two last points are just to keep the example code simple. A way that is aligned to standard PHP JSON serialization of a SimpleXMLElement is given in a series of blog posts of mine.
Basics of exactly this procedure and an exemplary JsonSerialize implementation is available in the third post: SimpleXML and JSON Encode in PHP – Part III and End.
Another related question is:
PHP convert XML to JSON group when there is one child
$fileContents= file_get_contents("https://www.feedforall.com/sample.xml");
$fileContents = str_replace(array("\n", "\r", "\t"), '', $fileContents);
$fileContents = trim(str_replace('"', "'", $fileContents));
$simpleXml = simplexml_load_string($fileContents);
$json = json_encode($simpleXml);
$array = json_decode($json,TRUE); // convert the JSON-encoded string to a PHP variable
return $array;
I'ts Better example:
I want to know if passing an XML node and then calling upon a method to access it is legal syntax in PHP. I tried converting to string, but that didn't work.
What am I doing wrong?
What would be the best/simplest alternative?
XML
<user>
<widgets>
<widget>Widget 1</widget>
<stuff>
<morestuff>Things</morestuff>
</stuff>
<stuff>
<morestuff>Things</morestuff>
</stuff>
<widget>Widget 2</widget>
</widgets>
</user>
PHP
<?php
$xmlfile = 'widgets/widgets_files/widgets.xml';
$widgets = array();
$user = new SimpleXMLElement($xmlfile, NULL, true);
$dom = new DOMDocument('1.0');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom = dom_import_simplexml($user)->ownerDocument;
foreach ($user->widgets->widget as $widget) {
$new_widget = new Widget($widget); //Where the node gets passed
array_push($widgets, $new_widget);
}
//For example
$new_widget[0]->set_subnodes();
$new_widget[0]->get_subnodes();
class Widget {
private $widget;
private $stuffArray = array();
public function __construct($widget) {
$this->widget = $widget;
}
public function set_subnodes() {
foreach ($this->widget->stuff->morestuff as $morestuff => $value) {
$this->stuffArray[$morestuff] = $value;
}
}
public function get_subnodes() {
foreach ($this->stuffArray as $stuff) {
echo$stuff;
}
}
}
It is indeed possible to pass XML objects as parameters to objects and to call methods on them, but there are a number of errors in your code which are stopping it from working. In particular, the XML that you are using isn't the structure that you think it is--the stuff and morestuff nodes are not children of widget, so none of the actions that you're trying to perform with them will work. Here's a corrected version of the XML and some PHP code that does what I think you're trying to do above:
$widgets = array();
# you can load your code from a file, obviously--for the purposes of the example,
# I'm loading mine using a function.
$sxe = simplexml_load_string( get_my_xml() );
foreach ($sxe->widgets->widget as $widget) {
$new_widget = new Widget($widget); // Where the node gets passed
array_push($widgets, $new_widget);
}
// For example
foreach ($widgets as $w) {
$w->set_subnodes();
$w->get_subnodes();
}
function get_my_xml() {
return <<<XML
<user>
<widgets>
<widget>Widget 1
<stuff>
<morestuff>Things</morestuff>
</stuff>
<stuff>
<morestuff>Other Things</morestuff>
</stuff>
</widget>
<widget>Widget 2
<stuff>
<morestuff>Widget Two's Things</morestuff>
</stuff>
<stuff>
<morestuff>Widget Two's Other Things</morestuff>
</stuff>
</widget>
</widgets>
</user>
XML;
}
The Widget object:
class Widget {
private $widget;
private $stuffArray = array();
public function __construct($widget) {
$this->widget = $widget;
}
public function set_subnodes() {
# put all the "morestuff" nodes into the stuffArray
foreach ($this->widget->xpath("stuff/morestuff") as $ms) {
print "pushing $ms on to array" . PHP_EOL;
array_push($this->stuffArray, $ms);
}
}
public function get_subnodes() {
foreach ($this->stuffArray as $stuff) {
print "Running get_subnodes: got $stuff" . PHP_EOL;
}
}
}
Output:
pushing Things on to array
pushing Other Things on to array
Running get_subnodes: got Things
Running get_subnodes: got Other Things
pushing Widget Two's Things on to array
pushing Widget Two's Other Things on to array
Running get_subnodes: got Widget Two's Things
Running get_subnodes: got Widget Two's Other Things
I am reading in a an xml file which returns me a SimpleXMLElement Object representation of the xml. I am going to take an array and feed in new values to that object. I don't know what I am going to be in that array.
if I were to brute force this I would do something like this.
//Solution 1: Brute Force
//Just creating an array and value for purposes of demonstration.
$arOfData = array( [0]=>"theFirstNode", [1]=>"theSecondNode",[2]=>"theThirdNode" );
$value = "The XML Node Value";
$simpleXml->$arOfData[0]->$arOfData[1]->$arOfData[2] = $value;
//The next best thing I can think of doing is something like this.
//Solution 2: Semi-brute force
//
foreach($this->arrayData as $key => $value) {
$xmlNodes = explode( '-', $key);
$numNodes = count($xmlNodes);
switch($numNodes) {
case 1:
$simpleXml->$xmlNodes[0] = $value;
break;
case 2:
$simpleXml->$xmlNodes[0]->$xmlNodes[1] = $value;
break;
case 3:
$simpleXml->$xmlNodes[0]->$xmlNodes[1]->$xmlNodes[2] = $value;
break;
case 4:
$simpleXml->$xmlNodes[0]->$xmlNodes[1]->$xmlNodes[2]->$xmlNodes[3] = $value;
break;
case 5:
$simpleXml->$xmlNodes[0]->$xmlNodes[1]->$xmlNodes[2]->$xmlNodes[3]->$xmlNodes[4] = $value;
break;
}
}
*note This solution uses the array key and explodes it to an array delimited by a dash and then uses the array value as the new xml value. So don't let that distract you.
The problem with solution #2 is: what happens when we get a xml node that is deeper than 5? Its not going to be stuffed into our new object we are creating. Oh oh. It's also not very elegant ;). I am not sure how to do this in a more recursive manner.
Like you already wrote in your question, you need to have this dynamically because you do not know about the number of parent elements.
You need to dig a little deeper into how simpexml works to get this done.
But first let me suggest you to have a different notation, not with the minus sign you have but with a slash like in a path.
first/second/third
This is also common with Xpath and I think it's pretty well speaking for itself. Also the minus sign can be part of an element name, but the slash can not. So this is just a bit better.
Before I show you how you can easily access that <third> element node to set its value, first lets look at some assignment basics in simplexml.
To access and set this element-node in a SimpleXMLElement see the following example:
$xml = new SimpleXMLElement('<root><first><second><third/></second></first></root>');
$element = $xml->first->second->third;
$element[0] = "value";
This is pretty straight forward but you can see two things here:
The <third> element already exists in the document.
The code uses as simplexml-self-reference ([0]) which allows to set the XML value of the element variable (and not the variable). This is specific to how SimpleXMLElement works.
The second point also contains the solution to the problem how to deal with non-existent elements. $element[0] is NULL in case the element does not exists:
$xml = new SimpleXMLElement('<root><first><second/></first></root>');
$element = $xml->first->second->third;
var_dump($element[0]); # NULL
So let's try to conditionally add the third element in case it does not exists:
if ($xml->first->second->third[0] === NULL) {
$xml->first->second->third = "";
}
This does solve that problem. So the only thing left to do is to do that in an iterative fashion for all parts of the path:
first/second/third
To keep this easy, create a function for this:
/**
* Modify an elements value specified by a string-path.
*
* #param SimpleXMLElement $parent
* #param string $path
* #param string $value (optional)
*
* #return SimpleXMLElement the modified element-node
*/
function simplexml_deep_set(SimpleXMLElement $parent, $path, $value = '')
{
### <mocked> to be removed later: ###
if ($parent->first->second->third[0] === NULL) {
$parent->first->second->third = "";
}
$element = $parent->first->second->third;
### </mocked> ###
$element[0] = $value;
return $element;
}
Because the function is mocked, it can be used directly:
$xml = new SimpleXMLElement('<root><first><second/></first></root>');
simplexml_deep_set($xml, "first/second/third", "The XML Node Value");
$xml->asXML('php://output');
And this works:
<?xml version="1.0"?>
<root><first><second><third>The XML Node Value</third></second></first></root>
So now removing the mock. First insert the explode like you have it as well. Then all that needs to be done is to go along each step of the path and create the element conditionally if it yet does not exist. In the end $element will be the element to modify:
$steps = explode('/', $path);
$element = $parent;
foreach ($steps as $step)
{
if ($element->{$step}[0] === NULL) {
$element->$step = '';
}
$element = $element->$step;
}
This foreach is needed to replace the mock with a working version. Compare with the full function definition at a glance:
function simplexml_deep_set(SimpleXMLElement $parent, $path, $value = '')
{
$steps = explode('/', $path);
$element = $parent;
foreach ($steps as $step)
{
if ($element->{$step}[0] === NULL) {
$element->$step = "";
}
$element = $element->$step;
}
$element[0] = $value;
return $element;
}
Lets modify more crazy things to test it out:
$xml = new SimpleXMLElement('<root><first><second/></first></root>');
simplexml_deep_set($xml, "first/second/third", "The XML Node Value");
simplexml_deep_set(
$xml, "How/do/I/dynamically/create/a/php/simplexml/object/while/keeping/current/properties"
, "The other XML Node Value"
);
$xml->asXML('php://output');
Example-Output (beautified):
<?xml version="1.0"?>
<root>
<first>
<second>
<third>The XML Node Value</third>
</second>
</first>
<How>
<do>
<I>
<dynamically>
<create>
<a>
<php>
<simplexml>
<object>
<while>
<keeping>
<current>
<properties>The other XML Node Value</properties>
</current>
</keeping>
</while>
</object>
</simplexml>
</php>
</a>
</create>
</dynamically>
</I>
</do>
</How>
</root>
See it in action.
Simple XML templates like these ones :
structure.xml :
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<document>
<book>first book</book>
<book>second book</book>
((other_books))
</document>
book_element.xml :
<book>((name))</book>
And this test :
<?php
Header("Content-type: text/xml; charset=UTF-8");
class XMLTemplate extends DOMDocument
{
private $_content_storage;
private $_filepath;
private $_tags;
public function XMLTemplate( $sFilePath )
{
if( !file_exists( $sFilePath ) ) throw new Exception("file not found");
$this->_filepath = $sFilePath;
$this->_tags = [];
$this->_content_storage = file_get_contents( $this->_filepath );
}
public function Get()
{
$this->merge();
$this->loadXML( $this->_content_storage );
return $this->saveXML();
}
public function SetTag( $sTagName, $sReplacement )
{
$this->_tags[ $sTagName ] = $sReplacement;
}
private function merge()
{
foreach( $this->_tags as $k=>$v)
{
$this->_content_storage = preg_replace(
"/\({2}". $k ."\){2}/i",
$v,
$this->_content_storage
);
}
$this->_content_storage = preg_replace(
"/\({2}[a-z0-9_\-]+\){2}/i",
"",
$this->_content_storage
);
}
}
$aBooks = [
"troisième livre",
"quatrième livre"
];
$Books = "";
foreach( $aBooks as $bookName )
{
$XMLBook = new XMLTemplate("book_element.xml");
$XMLBook->SetTag( "name", $bookName );
$Books .= $XMLBook->Get();
}
$XMLTemplate = new XMLTemplate("test.xml");
$XMLTemplate->SetTag("other_books", $Books);
echo $XMLTemplate->Get();
?>
Give me error :
Warning: DOMDocument::loadXML(): XML declaration allowed only at the start of the document in Entity, line: 5
Because loadXML() method add automatically the declaration to the content, but i need to inject parts of xml in the final template like above. How to disable this annoying auto adding and let me use my declaration ? Or another idea to conturn the problem ?
If you dislike the error and you want to save the document you'd like to merge without the XML declaration, just save the document element instead of the whole document.
See both variants in the following example-code (online-demo):
$doc = new DOMDocument();
$doc->loadXML('<root><child/></root>');
echo "The whole doc:\n\n";
echo $doc->saveXML();
echo "\n\nThe root element only:\n\n";
echo $doc->saveXML($doc->documentElement);
The output is as followed:
The whole doc:
<?xml version="1.0"?>
<root><child/></root>
The root element only:
<root><child/></root>
This probably should be already helpful for you. Additionally there is a constant for libxml which is said can be used to control whether or not the XML declaration is output. But I never used it:
LIBXML_NOXMLDECL (integer)
Drop the XML declaration when saving a document
Note: Only available in Libxml >= 2.6.21
From: http://php.net/libxml.constants
See the link for additional options, you might want to use the one or the other in the future.