2

I'm trying to create XML from an array in the following format

<root>
    <kyero>
        <feed_version>3</feed_version>
    </kyero>
    <property>
        <id>12367</id>
        <date>2013-09-27 12:00:10</date>
        <ref>V3TEST</ref>
        <price>250000</price>
        <currency>EUR</currency>
        <price_freq>sale</price_freq>
        <leasehold>0</leasehold>
        <type>villa</type>
        <town>almunecar</town>
        <province>granada</province>
        <country>spain</country>
        <location>
            <latitude>36.728807</latitude>
            <longitude>-3.693466</longitude>
        </location>
        <features>
            <feature>good rental potential</feature>
            <feature>near beach</feature>
            <feature>water possible</feature>
        </features>
        <notes>Private property notes</notes>
        <images>
            <image id="1">
                <url>http://images.kyero.com/12811577_large.jpg</url>
            </image>
            <image id="2">
                <url>http://images.kyero.com/12811578_large.jpg</url>
            </image>
            <image id="3">
                <url>http://images.kyero.com/12811579_large.jpg</url>
            </image>
            <image id="4">
                <url>http://images.kyero.com/12811581_large.jpg</url>
            </image>
            <image id="50">
                <url>http://images.kyero.com/12811582_large.jpg</url>
            </image>
        </images>
    </property>
</root>

The important take away is the nested arrays such as <features> and <images>.

An example of my array that I've built so far is:

Array
(
    [kyero] => Array
        (
            [feed_version] => 3
        )

    [0] => Array
        (
            [id] => 2024
            [ref] => jtl-2024-4202
            [date] => 2019-11-19 15:34:39
            [title] => A wonderful property
            [price] => 14828
            [price_freq] => month
            [country] => United Kingdom
            [location] => Array
                (
                    [latitude] => 55.311512
                    [longitude] => -2.9378154999999
                )

            [features] => Array
                (
                    [0] => Prominent Building
                    [1] => Great Location
                    [2] => Grade A Office Space
                    [3] => Air Conditioning
                    [4] => Generous Car Parking
                )

            [images] => Array
                (
                    [0] => Array
                        (
                            [url] => https://example.com/image1.jpg
                        )

                    [1] => Array
                        (
                            [url] => https://example.com/image2.jpg
                        )

                )

        )

)

And the code to convert the array to XML

$xml_data = new SimpleXMLElement('<?xml version="1.0"?><root></root>');
//$data = array as shown above 
array_to_xml($data, $xml_data);

The specific issue lies within this method

function array_to_xml($data, &$xml_data, $isImg = FALSE, $isFeature = FALSE) {    
    $i = 1;
    foreach ($data as $key => $value) {
        if (is_numeric($key)) {
            $key = 'property'; //dealing with <0/>..<n/> issues
            if($isImg){
                $key = 'image';
            }
            if($isFeature){
                $key = 'feature';
            }

        }
        if (is_array($value)) {
            if($key == 'images'){
                $isImg = true;
            } elseif($key == 'features'){
                $isFeature = true;
            }
            $subnode = $xml_data->addChild($key);
            if($key == 'image'){
                $subnode->addAttribute('id', $i);
            }
            array_to_xml($value, $subnode, $isImg, $isFeature);
        } else {
            $xml_data->addChild("$key", htmlspecialchars("$value"));
        }
        $i++;
    }
}

The output of the XML comes out as desired except for a few parts, specifically the nested parts like features and images.Note how each <url> is wrapped in a <feature> node. This should be a <image> node instead like shown in the originally XML shown at the start of this question, in addition to the id attributes on the image nodes as well. (the <location> doesn't suffer from the same problem)

<features>
    <feature>Prominent Building</feature>
    <feature>Great Location</feature>
    <feature>Grade A Office Space</feature>
    <feature>Air Conditioning</feature>
    <feature>Generous Car Parking</feature>
</features>
<images>
    <feature>
        <url>
        https://example.com/image1.jpg
        </url>
    </feature>
    <feature>
        <url>
        https://example.com/image2.jpg
        </url>
     </feature>
</images>

If I comment out

if($isFeature){
    $key = 'feature';
}

and

elseif($key == 'features'){
    $isFeature = true;
}

The images then are formatted correctly

<images>
    <image id="1">
        <url>
        https://example.com/image1.jpg
        </url>
    </image>
    <image id="2">
        <url>
        https://example.com/image2.jpg
        </url>
     </image>
</images>

but the features are incorrect, instead they become

<features>
    <property>Prominent Building</property>
    <property>Great Location</property>
    <property>Grade A Office Space</property>
    <property>Air Conditioning</property>
    <property>Generous Car Parking</property>
</features>

The <property> nodes should be <feature> nodes instead.

Could you help me with the array_to_xml() method to handle these and other nested arrays correctly?

Original conversion code: https://stackoverflow.com/a/5965940/10884442

GBWDev
  • 576
  • 5
  • 18

2 Answers2

2

Don't CONVERT, but CREATE. In other words do not try to build and array and implement a generic mapper. It is a lot more difficult. SimpleXML does not allow much control. In DOM here are 3 steps:

  1. Create the node using DOMDocument::create...() methods
  2. Append/Insert the node to its parent node using DOMNode::appendChild()/DOMNode::insertBefore()
  3. Add attributes and child nodes to the node

Here is a small example:

$document = new DOMDocument('1.0', 'UTF-8');

$root = $document->appendChild($document->createElement('root'));
$kyero = $root->appendChild($document->createElement('kyero'));
$kyero->appendChild(
    $document->createElement('feed_version')
)->textContent = '3';

$document->formatOutput = TRUE;
echo $document->saveXML();

Output:

<root>
  <kyero>
    <feed_version>3</feed_version>
  </kyero>
</root>

DOMNode::appendChild() returns the appended node, so you can nest the create call.

Changing DOMNode::$textContent replaces all current child nodes with a single text node. Unlike the secondary argument for SimpleXMLElement::addChild()/DOMDocument::createElement() it will properly escape special characters.

I typically implement a method XMLAppendable::appendTo() for XML serializable objects. It allows you to keep the XML create snippets in a maintainable size and add some verification.

interface XMLAppendable {
    public function appendTo(DOMElement $parentNode): void;
}

class ImageList implements XMLAppendable {

    private $_images = [];

    public function __construct(array $images) {
        $this->_images = $images;
    }

    public function appendTo(DOMElement $parentNode): void {
       $document = $parentNode->ownerDocument;
       $images = $parentNode->appendChild(
           $document->createElement('images')
       );
       foreach ($this->_images as $id => $url) {
         $image = $images->appendChild($document->createElement('images'));
         $image->setAttribute('id', $id);
         $image
           ->appendChild($document->createElement('url'))
           ->textContent = $url;
       }
    }
}

$document = new DOMDocument('1.0', 'UTF-8');

$root = $document->appendChild(
    $document->createElement('root')
);
$property = $root->appendChild(
    $document->createElement('property')
);
$images = new ImageList(
    [
        '2' => 'http://images.kyero.com/12811578_large.jpg',
        '50' => 'http://images.kyero.com/12811582_large.jpg'
    ]
);
$images->appendTo($property);

$document->formatOutput = TRUE;
echo $document->saveXML();
ThW
  • 19,120
  • 3
  • 22
  • 44
2

I agree with ThW about creating the data directly, but I am also a fan of doing things simply and SimpleXML is a good fit with what you are trying to do.

As I don't know how you obtained the data you have so far I will code this using that array, but instead of trying to do a generic flexible version, this shows how easy it is to create the entire structure using SimpleXML.

All it does is use the SimpleXML ability to reference an element by using object notation, the code is quite repetitive, but all it is doing is setting elements form the data in your array...

$xml_data = new SimpleXMLElement('<?xml version="1.0"?><root></root>');

$xml_data->kyero->feed_version = $data['kyero']['feed_version'];

$newProperty = $xml_data->addChild("property");
$newProperty->id = $data[0]['id'];
$newProperty->ref = $data[0]['ref'];
$newProperty->date = $data[0]['date'];
$newProperty->title = $data[0]['title'];
$newProperty->price = $data[0]['price'];
$newProperty->price_freq = $data[0]['price_freq'];
$newProperty->country = $data[0]['country'];
$newProperty->location->latitude = $data[0]['location']['latitude'];
$newProperty->location->longitude = $data[0]['location']['longitude'];

foreach ( $data[0]['features'] as $key => $feature )   {
    $newProperty->features->feature[$key] = $feature;
}
$images = $newProperty->addChild("images");
foreach ( $data[0]['images'] as $key => $image )   {
    $newImage = $images->addChild("image");
    $newImage['id']= $key+1;
    $newImage->url = $image['url'];
}

using your sample data, this gives...

<?xml version="1.0"?>
<root>
  <kyero>
    <feed_version>3</feed_version>
  </kyero>
  <property>
    <id>2024</id>
    <ref>jtl-2024-4202</ref>
    <date>2019-11-19 15:34:39</date>
    <title>A wonderful property</title>
    <price>14828</price>
    <price_freq>month</price_freq>
    <country>United Kingdom</country>
    <location>
      <latitude>55.311512</latitude>
      <longitude>-2.9378154999999</longitude>
    </location>
    <features>
      <feature>Prominent Building</feature>
      <feature>Great Location</feature>
      <feature>Grade A Office Space</feature>
      <feature>Air Conditioning</feature>
      <feature>Generous Car Parking</feature>
    </features>
    <images>
      <image id="1">
        <url>https://example.com/image1.jpg</url>
      </image>
      <image id="2">
        <url>https://example.com/image2.jpg</url>
      </image>
    </images>
  </property>
</root>
Nigel Ren
  • 56,122
  • 11
  • 43
  • 55
  • Coincidentally, this is what I ended up doing before you posted the answer. So much easier! – GBWDev Dec 05 '19 at 09:05