7

Up until now, I've been using the snippet below to convert an XML tree to an array:

$a = json_decode(json_encode((array) simplexml_load_string($xml)),1);

..however, I'm now working with an XML that has duplicate key values, so the array is breaking when it loops through the XML. For example:

<users>
    <user>x</user>
    <user>y</user>
    <user>z</user>
</users>

Is there a better method to do this that allows for duplicate Keys, or perhaps a way to add an incremented value to each key when it spits out the array, like this:

$array = array(
    users => array(
        user_1 => x,
        user_2 => y,
        user_3 => z
    )
)

I'm stumped, so any help would be very appreciated.

fireinspace
  • 197
  • 1
  • 1
  • 8
  • where is this being consumed? Changing array structure would seem like best approach – charlietfl Aug 02 '15 at 12:45
  • This is just an example xml. I'm working on a web service that accepts an incoming XML, so the structure is fixed. – fireinspace Aug 02 '15 at 12:50
  • Not normal for a web service to return dynamically created object keys though. `users => array( array( user=>x))` would make a lot more sense when consuming that data ... or just a flat array of values – charlietfl Aug 02 '15 at 12:53
  • for duplicate key it should be `$array = array( users => array(x, y, z)` Am i wrong? – splash58 Aug 02 '15 at 12:54
  • Casting a simplexml to an array is very fragile shortcut and works only in the narrowest of circumstances. You'll need to process the XML properly, iterate it to get the elements you need, or query with XPath then produce your output array. – Michael Berkowski Aug 02 '15 at 14:29

3 Answers3

2

Here is a complete universal recursive solution.

This class will parse any XML under any structure, with or without tags, from the simplest to the most complex ones.

It retains all proper values and convert them (bool, txt or int), generates adequate array keys for all elements groups including tags, keep duplicates elements etc etc...

Please forgive the statics, it s part of a large XML tools set I used, before rewriting them all for HHVM or pthreads, I havent got time to properly construct this one, but it will work like a charm for straightforward PHP.

For tags, the declared value is '@attr' in this case but can be whatever your needs are.

$xml = "<body>
             <users id='group 1'>
               <user>x</user>
               <user>y</user>
               <user>z</user>
             </users>
            <users id='group 2'>
               <user>x</user>
               <user>y</user>
               <user>z</user>
            </users>
        </body>";

$result = xml_utils::xml_to_array($xml);

result:

Array ( [users] => Array ( [0] => Array ( [user] => Array ( [0] => x [1] => y [2] => z ) [@attr] => Array ( [id] => group 1 ) ) [1] => Array ( [user] => Array ( [0] => x [1] => y [2] => z ) [@attr] => Array ( [id] => group 2 ) ) ) )

Class:

class xml_utils {

    /*object to array mapper */
    public static function objectToArray($object) {
        if (!is_object($object) && !is_array($object)) {
            return $object;
        }
        if (is_object($object)) {
            $object = get_object_vars($object);
        }
        return array_map('objectToArray', $object);
    }

    /* xml DOM loader*/
    public static function xml_to_array($xmlstr) {
        $doc = new DOMDocument();
        $doc->loadXML($xmlstr);
        return xml_utils::dom_to_array($doc->documentElement);
    }

    /* recursive XMl to array parser */
    public static function dom_to_array($node) {
        $output = array();
        switch ($node->nodeType) {
            case XML_CDATA_SECTION_NODE:
            case XML_TEXT_NODE:
                $output = trim($node->textContent);
                break;
            case XML_ELEMENT_NODE:
                for ($i = 0, $m = $node->childNodes->length; $i < $m; $i++) {
                    $child = $node->childNodes->item($i);
                    $v = xml_utils::dom_to_array($child);
                    if (isset($child->tagName)) {
                        $t = xml_utils::ConvertTypes($child->tagName);
                        if (!isset($output[$t])) {
                            $output[$t] = array();
                        }
                        $output[$t][] = $v;
                    } elseif ($v) {
                        $output = (string) $v;
                    }
                }
                if (is_array($output)) {
                    if ($node->attributes->length) {
                        $a = array();
                        foreach ($node->attributes as $attrName => $attrNode) {
                            $a[$attrName] = xml_utils::ConvertTypes($attrNode->value);
                        }
                        $output['@attr'] = $a;
                    }
                    foreach ($output as $t => $v) {
                        if (is_array($v) && count($v) == 1 && $t != '@attr') {
                            $output[$t] = $v[0];
                        }
                    }
                }
                break;
        }
        return $output;
    }

    /* elements converter */
    public static function ConvertTypes($org) {
        if (is_numeric($org)) {
            $val = floatval($org);
        } else {
            if ($org === 'true') {
                $val = true;
            } else if ($org === 'false') {
                $val = false;
            } else {
                if ($org === '') {
                    $val = null;
                } else {
                    $val = $org;
                }
            }
        }
        return $val;
    }

}
cpugourou
  • 775
  • 7
  • 11
  • I haven't tested this yet but its a sick looking script. At the very least it will probably teach me a thing or two. I'll follow up this evening and let you know how it works. Thanks! – fireinspace Aug 03 '15 at 15:01
0

You can loop through each key in your result and if the value is an array (as it is for user that has 3 elements in your example) then you can add each individual value in that array to the parent array and unset the value:

foreach($a as $user_key => $user_values) {
    if(!is_array($user_values))
        continue; //not an array nothing to do

    unset($a[$user_key]); //it's an array so remove it from parent array

    $i = 1; //counter for new key
    //add each value to the parent array with numbered keys
    foreach($user_values as $user_value) {
        $new_key = $user_key . '_' . $i++; //create new key i.e 'user_1'
        $a[$new_key] = $user_value; //add it to the parent array
    }
}

var_dump($a);
FuzzyTree
  • 32,014
  • 3
  • 54
  • 85
0

First of all this line of code contains a superfluous cast to array:

$a = json_decode(json_encode((array) simplexml_load_string($xml)),1);
                             ^^^^^^^

When you JSON-encode a SimpleXMLElement (which is returned by simplexml_load_string when the parameter could be parsed as XML) this already behaves as-if there would have been an array cast. So it's better to remove it:

$sxml  = simplexml_load_string($xml);
$array = json_decode(json_encode($sxml), 1);

Even the result is still the same, this now allows you to create a subtype of SimpleXMLElement implementing the JsonSerialize interface changing the array creation to your needs.

The overall method (as well as the default behaviour) is outlined in a blog-series of mine, on Stackoverflow I have left some more examples already as well:

Your case I think is similar to what has been asked in the first of those three links.

Community
  • 1
  • 1
hakre
  • 193,403
  • 52
  • 435
  • 836