Flatten Multidimensional Arrays in PHP

So you've probably heard this story a million times. Someone points you to an XML feed and says, "Can you deliver it to these guys daily?" You reflexively say, "Sure thing, no problemo, good buddy!" Before leaving you to your job, they turn around and say, "Oh, and they need it as a CSV." But as you inspect the feed, your eyes start to glaze over as you discover a complex system of layered information, some of it enumerated, some keyed. You start to sweat at the thought of maintaining yet another custom data parser, liable to break the moment some jerk decides they really want their field names to reflect the latest trends in industry terminology. The thought then dawns on you that you can't possibly be the first person to have this problem. You can google with the best of them, you assure yourself; but as you read one stack overflow or php.net post after the next, your hope begins to fade as you witness the disappointing quality of their output.

Can it really be that hard to take a multi-layered array of information, and flatten it down to a single-layered array with thoughtfully-named keys, suitable for output to a simple format like CSV? Well, unless your googling skills are just far superior to mine, or maybe you were fortunate to have some excellent mentors, I'm going to say, "Yes! It is freaking hard!" After much trial and even more error, I've finally become satisfied with the output of the following function:

//Don't recall who wrote this, but it's a handy little one-liner
function is_assoc($array) {
  return (bool)count(array_filter(array_keys($array), 'is_string'));
}

/***
 * @name array_flatten
 * @author Tom Penzer @tpenzer
 * Flattens a multi-tiered itemized array into a single-tiered itemized 
 * associative array with keys reflective of their values' hierarchy.
 *
 * @param    array    $items    Required - the itemized multi-level
 * keyed array to be flattened
 * @param    string   $prefix   Optional - the prefix to be added to 
 * all keys at the current level - used when called recursively for
 * labeling of sub-level values
 *
 * @return    array    an itemized single-level keyed array
 ***/
function array_flatten($items, $prefix = NULL) {
    $array = array();
    
    foreach ($items as $key => $value) {
        if (is_array($value)) {
            if (empty($value)) {
                $array[$key] = '';
            } else if ( ! is_assoc($value)) {
		if (empty($prefix)) {
			$this_prefix = $key . '_';
		} else {
			$this_prefix = $prefix;
                }
                foreach ($value as $position => $data) {
                    if (is_array($data)) {
                        if (empty($data)) {
                            $array[$this_prefix . $position] = '';
                        } else {
                            $partial[$key] = $data;
                            $array = array_merge($array, array_flatten($partial, $key . '_' . $position . '_'));
                        }
                    } else {
                        $array[$this_prefix . $position] = $data;
                    }
                }
            } else {
		if (empty($prefix)) {
			$this_prefix = $key . '_';
		} else {
			$this_prefix = $prefix;
                }
                foreach ($value as $subkey => $data) {
                    if (is_array($data)) {
                        if (empty($data)) {
                            $array[$this_prefix . $subkey] = '';
                        } else {
                            $partial[$key] = $data;
                            $array = array_merge($array, array_flatten($partial, $key . '_'));
                        }
                    } else {
                        $array[$this_prefix . $subkey] = $data;
                    }
                }
            }
        } else {
            $array[$key] = $value;
        }
    }
    
    return $array;
}

So after you parse the XML into an object, convert the object into an array, you can then pass it on to array_flatten(), and output to CSV, without worrying about small details like the fact that there are two fields named 'price', with one under 'shipping' and the other at the root level of each item, or that there are five enumerated 'additional_image' sub-fields. No, you can rest easy and not worry about missing any of the latest geek news, secure in the notion that you chose to use the array flattener to end all array flatteners.

You are free to use the code in this post for any purpose.