12

Here is an example array:

 $foo = array(
           'employer' => array(
                    'name' => 'Foobar Inc',
                    'phone' => '555-555-5555'
                     ),
           'employee' => array(
                    'name' => 'John Doe',
                    'phone' => '555-555-5556',
                    'address' => array(
                           'state' => 'California',
                           'zip' => '90210'
                        )
                    ),
           'modified' => '2009-12-01',
         );

And I would like to get a result like this:

$fooCompressed = array(
             'employer_name' => 'Foobar Inc',
             'employer_phone' => '555-555-5555',
             'employee_name' => 'John Doe',
             'employee_phone' => '555-555-5556'
             'employee_address_state' => 'California',
             'employee_address_zip' => '90210',
             'modified' => '2009-12-01'
             )

How would I go about writing a recursive function to handle this?

Tom Penzer
  • 103
  • 1
  • 5
GSto
  • 41,512
  • 37
  • 133
  • 184

8 Answers8

17

Something like this:

function makeNonNestedRecursive(array &$out, $key, array $in){
    foreach($in as $k=>$v){
        if(is_array($v)){
            makeNonNestedRecursive($out, $key . $k . '_', $v);
        }else{
            $out[$key . $k] = $v;
        }
    }
}

function makeNonNested(array $in){
    $out = array();
    makeNonNestedRecursive($out, '', $in);
    return $out;
}

// Example
$fooCompressed = makeNonNested($foo);
VDVLeon
  • 1,393
  • 1
  • 15
  • 26
  • +1 This is pretty close to what I'd do. Because the keys are being modified, there is no built-in function that will do it for you, and you definitely need recursion to drill down through any sub-values that are also arrays. – zombat Dec 10 '09 at 18:22
8

I think this 'trick' using is http_build_query is less of an eyesore w/out recursion (or at least letting php do it for you)

3 lines of code if your str_replace uses the url-encoded values for [ and ]

$string      = http_build_query($array);
$string      = urldecode($string);
$string      = str_replace(
                    array('[',']'),
                    array('_','') , 
                    $string
                );
parse_str($string, $flat_array);

$flat_array becomes :

array(7) {
  ["employer_name"]         =>"Foobar Inc"
  ["employer_phone"]        =>"555-555-5555"
  ["employee_name"]         =>"John Doe"
  ["employee_phone"]        =>"555-555-5556"
  ["employee_address_state"]=>"California"
  ["employee_address_zip"]  =>"90210"
  ["modified"]              =>"2009-12-01"
}
yak
  • 340
  • 3
  • 7
4

Here is a function which allows you to specify a top-level prefix via the second parameter:

function flatten_array($array, $prefix = null) {
  if ($prefix) $prefix .= '_';

  $items = array();

  foreach ($array as $key => $value) {
    if (is_array($value))
      $items = array_merge($items,  flatten_array($value, $prefix . $key));
    else
      $items[$prefix . $key] = $value;
  }

  return $items;
}
user229044
  • 232,980
  • 40
  • 330
  • 338
3

Approach I liked more is quite similar to some posted here but not equal. I found it into a duped post: https://stackoverflow.com/a/9546215/4791386 by user "Felix Kling"

His code flattens array keys resulting single dimension array with dot concatenated keys, which implies that numerical arrays will creates his own "key paths". This is very useful, but in large amount of similar items inside array could result a ton of meaningless similar paths.

function flatten($array, $prefix = '') {
    $result = array();
    foreach($array as $key=>$value) {
        if(is_array($value)) {
            $result = $result + flatten($value, $prefix . $key . '.');
        }
        else {
            $result[$prefix . $key] = $value;
        }
    }
    return $result;
}

In my case, I also needed a "unique like" path flattening as array key, and a sample of the data I could spec. So I extend his approach adding a numeric key squashing optional parameter. Also added optional parameter separator configuration.

The main purpose is make easy to analyze key structure and path related data. I think this method is useful when intended task is key mapping for further full data operations.

/**
* Convert a multidimensional array into a single dimension array.
* Nested array keys will be concatenated with the $separator string
* Numeric keys can also be flattened in a "unique key" array style with $numeric_squash 
* If $numeric_squash is true, numeric array keys are concatenated with $numeric_squash_separator, 
* for later detection and processing if necessary. "[*]" by default.
* If $numeric_squash_separator is set to false, the array key is flattened so that the values 
* would be displayed as if there were no numeric array.
*
* array  $array                    : Array to be flattened
* string $prefix                   : String to prepend on flattened keys
* string $separator                : String concatenated between nested array keys.
* bool   $numeric_squash           : Squash numeric array keys
* string $numeric_squash_separator : String replacing numeric keys, none if false

*/  

public static function array_flatten($array, $prefix = '', $separator = '.' , $numeric_squash = false , $numeric_squash_separator = '[*]') {
    $result = array();
    foreach($array as $key => $value) {
        if(is_array($value)) {
            if($numeric_squash && is_numeric($key))
                $n_key = $numeric_squash_separator ? $numeric_squash_separator . $separator: '';
            else
                $n_key = $key . $separator;

            $result = $result + self::array_flatten($value, $prefix . $n_key  , $separator , $numeric_squash , $numeric_squash_separator);
        }
        else {
            $result[$prefix . ($numeric_squash && is_numeric($key) ? '' : $key)] = $value;
        }
    }
    return $result;
}

Also say that this function is not performance optimized, iterations can be saved on numeric_squash and also some compare operations I think.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
1

A solution whith only array_* php functions + recursive :

<?php
$array = array(
    "level1"=>"value",
    "level2" => ["level11" => "value", "level21" => "value"],
    "level3" => ["level2" => ["level1" => "value"]],
    "level4" => ["level3" => ["level2" => ["level1" => "value"]]],
    "level5" => ["level4" => ["level3" => ["level2" => ["level1" => "value"]]]],
);
class GharbiFlat {
 
    /**
     * flatten array with combined keys
     */
    public function arrayFlat($array, $keySeparator = '_')
    {
        $result = [];
        array_walk(
            $array,
            function ($v, $pk) use (&$result, $keySeparator) {
                if (is_array($v)) {
                    $result += $this->arrayFlat(
                        array_combine(
                            array_map(
                                function ($k) use ($pk, $keySeparator) {
                                    return $pk . $keySeparator . $k;
                                },
                                array_keys($v)
                            ),
                            $v
                        ),
                        $keySeparator
                    );
                } else {
                    $result[$pk] = $v;
                }
            }
        );
        return $result;
    }
}

$example = new GharbiFlat();

print_r($example->arrayFlat($array));

Output :

Array
(
    [level1] => value
    [level2_level11] => value
    [level2_level21] => value
    [level3_level2_level1] => value
    [level4_level3_level2_level1] => value
    [level5_level4_level3_level2_level1] => value
)
Mohamed23gharbi
  • 1,710
  • 23
  • 28
0
/**
 * Flatten a multi-dimensional array or a nested object, constructing concatenated keys for
 *    nested elements.
 * @param array or object $array - the array or object to be flattened
 * @param array or string $key_path - current parent keys path.
 *    Pass this parameter as string if you need to set a common prefix for all keys 
 * @param string $level_separator - keys concatenation glue
 * @param array $flat - resulting flattened array (omit this parameter when calling the function)
 * @return single-dimensional array with all array keys as concatenated keys of elements' 
 *    paths through the data structure
 */
 function flattenArray($array, &$key_path = array(), $level_separator = '.', &$flat = array())
 {
      if(!is_array($key_path))
      {
           // sanitize key_path
           $key_path = array((string)$key_path);
       }
       foreach($array as $key => $value)
       {
            // push current key to path
            array_push($key_path, $key);

            if(is_array($value) || is_object($value))
            {
                 // next level recursion
                 $flat = array_merge($flat, flattenArray($value, $key_path, $level_separator, $flat));
             }
             else
             {
                  // write the value directly
                  $flat[implode($level_separator, $key_path)] = $value;
              }

              // remove used key
              array_pop($key_path);
        }

        return $flat;
  }
Alex
  • 1
0

After a few iterations, I've been able to refine a solution to this problem that uses a stack-based approach to avoid recursion, simplifying things a bit.

/***
 * @name array_flatten
 * @author Tom Penzer @tpenzer
 * Flattens a multi-tiered array into a single-tiered 
 * associative array with keys reflective of their 
 * values' hierarchy.
 *
 * @param    array    $array       Required - the multi- 
 * level keyed array to be flattened
 * @param    string   $separator   Optional - the string 
 * used to separate the keys from different levels of 
 * the hierarchy
 *
 * @return   array    a single-level keyed array
 ***/
function array_flatten($array, $separator = '_') {
    $output = array();

    while (list($key, $value) = each($array)) {
        if (is_array($value)) {
            $build = array();
            foreach ($value as $s_key => $s_value) {
                $build[$key . $separator . $s_key] = $s_value;
            }
            unset($array[$key]);
            $array = $build + $array;
            unset($build);
            continue;//skip write to $output
        }
        $output[$key] = $value;
        unset($array[$key]);
    }

    return $output;
}

Not exactly the method requested, but it's a nice contrast to the recursive approaches to the problem.

Tom Penzer
  • 103
  • 1
  • 5
-2

This will flatten a multidimensional associative array tacking a digit to the key if its a duplicate. If you don't mind having a digit index to differentiate duplicate keys instead of concatenated keys this could be a solution.

$result = array();
array_walk_recursive($your_array, function($v, $k) use (&$result){ $i = ""; for (; isset($result[$k."$i"]); $i++); $result[$k."$i"] = $v; });

I suspect it could be worked on further to do concatenated keys.

The above solution is basically for doing this kind of thing

<?php
    $xml_str = "
    <images>
        <image>
            <position>0</position>                  
        </image>
        <image1>
            <position>10</position>
        </image1>
    </images>";
                    // turn the xml into a multidimentional array
            $ob = simplexml_load_string($xml_str);
            $json = json_encode($ob);
            $my_array = json_decode($json, true);

            print_r($my_array);
                    // flatten it
            $result = array();
            array_walk_recursive($my_array, function($v, $k) use (&$result){ $i = ""; for (; isset($result[$k."$i"]); $i++); $result[$k."$i"] = $v; });

            print_r($result);
?>
Anthony
  • 575
  • 6
  • 8
  • 1
    `array_walk_recursive()` ignores all non-leafnodes. https://3v4l.org/Ca4m2 This technique cannot possibly deliver the required output for this question. To rephrase, this is the correct answer to a different question. – mickmackusa Jan 03 '22 at 21:40