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?
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);
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"
}
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;
}
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.
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
)
/**
* 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;
}
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.
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);
?>
Related
This question already has answers here:
How to access and manipulate multi-dimensional array by key names / path?
(10 answers)
Closed last year.
I have an unusual use-case I'm trying to code for. The goal is this: I want the customer to be able to provide a string, such as:
"cars.honda.civic = On"
Using this string, my code will set a value as follows:
$data['cars']['honda']['civic'] = 'On';
It's easy enough to tokenize the customer input as such:
$token = explode("=",$input);
$value = trim($token[1]);
$path = trim($token[0]);
$exploded_path = explode(".",$path);
But now, how do I use $exploded path to set the array without doing something nasty like an eval?
Use the reference operator to get the successive existing arrays:
$temp = &$data;
foreach($exploded as $key) {
$temp = &$temp[$key];
}
$temp = $value;
unset($temp);
Based on alexisdm's response :
/**
* Sets a value in a nested array based on path
* See https://stackoverflow.com/a/9628276/419887
*
* #param array $array The array to modify
* #param string $path The path in the array
* #param mixed $value The value to set
* #param string $delimiter The separator for the path
* #return The previous value
*/
function set_nested_array_value(&$array, $path, &$value, $delimiter = '/') {
$pathParts = explode($delimiter, $path);
$current = &$array;
foreach($pathParts as $key) {
$current = &$current[$key];
}
$backup = $current;
$current = $value;
return $backup;
}
Well tested and 100% working code. Set, get, unset values from an array using "parents". The parents can be either array('path', 'to', 'value') or a string path.to.value. Based on Drupal's code
/**
* #param array $array
* #param array|string $parents
* #param string $glue
* #return mixed
*/
function array_get_value(array &$array, $parents, $glue = '.')
{
if (!is_array($parents)) {
$parents = explode($glue, $parents);
}
$ref = &$array;
foreach ((array) $parents as $parent) {
if (is_array($ref) && array_key_exists($parent, $ref)) {
$ref = &$ref[$parent];
} else {
return null;
}
}
return $ref;
}
/**
* #param array $array
* #param array|string $parents
* #param mixed $value
* #param string $glue
*/
function array_set_value(array &$array, $parents, $value, $glue = '.')
{
if (!is_array($parents)) {
$parents = explode($glue, (string) $parents);
}
$ref = &$array;
foreach ($parents as $parent) {
if (isset($ref) && !is_array($ref)) {
$ref = array();
}
$ref = &$ref[$parent];
}
$ref = $value;
}
/**
* #param array $array
* #param array|string $parents
* #param string $glue
*/
function array_unset_value(&$array, $parents, $glue = '.')
{
if (!is_array($parents)) {
$parents = explode($glue, $parents);
}
$key = array_shift($parents);
if (empty($parents)) {
unset($array[$key]);
} else {
array_unset_value($array[$key], $parents);
}
}
$data = $value;
foreach (array_reverse($exploded_path) as $key) {
$data = array($key => $data);
}
Based on Ugo Méda's response :
This version
allows you to use it solely as a getter (leave the source array untouched)
fixes the fatal error issue if a non-array value is encountered (Cannot create references to/from string offsets nor overloaded objects)
no fatal error example
$a = ['foo'=>'not an array'];
arrayPath($a, ['foo','bar'], 'new value');
$a is now
array(
'foo' => array(
'bar' => 'new value',
),
)
Use as a getter
$val = arrayPath($a, ['foo','bar']); // returns 'new value' / $a remains the same
Set value to null
$v = null; // assign null to variable in order to pass by reference
$prevVal = arrayPath($a, ['foo','bar'], $v);
$prevVal is "new value"
$a is now
array(
'foo' => array(
'bar' => null,
),
)
/**
* set/return a nested array value
*
* #param array $array the array to modify
* #param array $path the path to the value
* #param mixed $value (optional) value to set
*
* #return mixed previous value
*/
function arrayPath(&$array, $path = array(), &$value = null)
{
$args = func_get_args();
$ref = &$array;
foreach ($path as $key) {
if (!is_array($ref)) {
$ref = array();
}
$ref = &$ref[$key];
}
$prev = $ref;
if (array_key_exists(2, $args)) {
// value param was passed -> we're setting
$ref = $value; // set the value
}
return $prev;
}
You need use Symfony PropertyPath
<?php
// ...
$person = array();
$accessor->setValue($person, '[first_name]', 'Wouter');
var_dump($accessor->getValue($person, '[first_name]')); // 'Wouter'
// or
// var_dump($person['first_name']); // 'Wouter'
This is exactly what this method is for:
Arr::set($array, $keys, $value);
It takes your $array where the element should be set, and accept $keys in dot separated format or array of subsequent keys.
So in your case you can achieve desired result simply by:
$data = Arr::set([], "cars.honda.civic", 'On');
// Which will be equivalent to
$data = [
'cars' => [
'honda' => [
'civic' => 'On',
],
],
];
What's more, $keys parameter can also accept creating auto index, so you can for example use it like this:
$data = Arr::set([], "cars.honda.civic.[]", 'On');
// In order to get
$data = [
'cars' => [
'honda' => [
'civic' => ['On'],
],
],
];
Can't you just do this
$exp = explode(".",$path);
$array[$exp[0]][$exp[1]][$exp[2]] = $value
I have an array that looks like this.
$array = [
0 => 'abc',
1 => [
0 => 'def',
1 => 'ghi'
],
'assoc1' => [
'nassoc' => 'jkl',
'nassoc2' => 'mno',
'nassoc3' => '',
'nassoc4' => false
]
];
The $array can have numeric keys or be an assoc array or a mixed one. The level of nesting is not known. Also the values of the array can also be bool or null or an empty string ''
I need to able to convert this into a scalar array with key value pairs. And then later reconvert it back to the exact same array.
So the scalar array could look like
$arrayScalar = [
'0' => 'abc',
'1[0]' => 'def',
'1[1]' => 'ghi',
'assoc1[nassoc]' => 'jkl',
'assoc1[nassoc2]' => 'mno',
'assoc1[nassoc3]' => '',
'assoc1[nassoc4]' => false
];
And then later be able to get back to the initial $array.
I wrote a parser and it does not currently handle bool values correctly.
I have a feeling this is at best a super hacky method to do what I am after. Also I have been able to test it only so much.
function flattenNestedArraysRecursively($nestedArray, $parent = '', &$flattened = [])
{
$keys = array_keys($nestedArray);
if (empty($keys)) {
$flattened[$parent] = 'emptyarray';
} else {
foreach ($keys as $value) {
if (is_array($nestedArray[$value])) {
$reqParent = (!empty($parent)) ? $parent . '|!|' . $value : $value;
$this->flattenNestedArraysRecursively($nestedArray[$value], $reqParent, $flattened);
} else {
$reqKey = (!empty($parent)) ? $parent . '|!|' . $value : $value;
$flattened[$reqKey] = $nestedArray[$value];
}
}
}
return $flattened;
}
function reCreateFlattenedArray($flatArray): array
{
$arr = [];
foreach ($flatArray as $key => $value) {
$keys = explode('|!|', $key);
$arr = $this->reCreateArrayRecursiveWorker($keys, $value, $arr);
}
return $arr;
}
function reCreateArrayRecursiveWorker($keys, $value, $existingArr)
{
//Outside to Inside
$keyCur = array_shift($keys);
//Check if keyCur Exists in the existingArray
if (key_exists($keyCur, $existingArr)) {
// Check if we have reached the deepest level
if (empty($keys)) {
//Return the Key => value mapping
$existingArr[$keyCur] = $value;
return $existingArr;
} else {
// If not then continue to go deeper while appending deeper levels values to current key
$existingArr[$keyCur] = $this->reCreateArrayRecursiveWorker($keys, $value, $existingArr[$keyCur]);
return $existingArr;
}
} else {
// If Key does not exists in current Array
// Check deepest
if (empty($keys)) {
//Return the Key => value mapping
$existingArr[$keyCur] = $value;
return $existingArr;
} else {
// Add the key
$existingArr[$keyCur] = $this->reCreateArrayRecursiveWorker($keys, $value, []);
return $existingArr;
}
}
}
Is there a better more elegant way of doing this, maybe http_build_query or something else I am not aware of.
Sandbox link -> http://sandbox.onlinephpfunctions.com/code/50b3890e5bdc515bc145eda0a1b34c29eefadcca
Flattening:
Your approach towards recursion is correct. I think we can make it more simpler.
We loop over the array. if the value is an array in itself, we recursively make a call to this new child subarray.
This way, we visit each key and each value. Now, we are only left to manage the keys to assign them when adding to our final resultant array, say $arrayScalar.
For this, we make a new function parameter which takes the parent key into account when assigning. That's it.
Snippet:
$arrayScalar = [];
function flatten($array,&$arrayScalar,$parent_key){
foreach($array as $key => $value){
$curr_key = empty($parent_key) ? $key : $parent_key . '[' . $key . ']';
if(is_array($value)){
flatten($value,$arrayScalar,$curr_key);
}else{
$arrayScalar[$curr_key] = $value;
}
}
}
flatten($array,$arrayScalar,'');
var_export($arrayScalar);
Demo: http://sandbox.onlinephpfunctions.com/code/1e3092e9e163330f43d495cc9d4acb672289a987
Unflattening:
This one is a little tricky.
You might have already noticed that the keys in the flattened array are of the form key1[key2][key3][key4] etc.
So, we collect all these individually in a new array, say $split_key. It might look like this.
array (
'key1',
'key2',
'key3',
'key4',
)
To achieve the above, we do a basic string parsing and added in-between keys to the array whenever we reach the end of the key string or [ or ].
Next, to add them to our final resultant array, we loop over the collected keys and check if they are set in our final array. If not so, set them. We now pass child array reference to our temporary variable $temp. This is to edit the same copy of the array. In the end, we return the result.
Snippet:
<?php
function unflatten($arrayScalar){
$result = [];
foreach($arrayScalar as $key => $value){
if(is_int($key)) $key = strval($key);
$split_key = [];
$key_len = strlen($key);
$curr = '';
// collect them as individual keys
for($i = 0; $i < $key_len; ++$i){
if($key[ $i ] == '[' || $key[ $i ] == ']'){
if(strlen($curr) === 0) continue;
$split_key[] = $curr;
$curr = '';
}else{
$curr .= $key[ $i ];
}
if($i === $key_len - 1 && strlen($curr) > 0){
$split_key[] = $curr;
}
}
// collecting them ends
//add them to our resultant array.
$temp = &$result;
foreach($split_key as $sk){
if(!isset($temp[ $sk ])){
$temp[ $sk ] = [];
}
$temp = &$temp[$sk];
}
$temp = $value;
}
return $result;
}
var_export(unflatten($arrayScalar));
Demo: http://sandbox.onlinephpfunctions.com/code/66136a699c3c5285eed3d3350ed4faa5bbce4b76
I'm trying to search in multidimensional array and get the value but return doesn't work.
function search($arr,$q){
foreach($arr as $key => $val){
if(trim($key) == $q) return $arr;
else if(is_array($val)) search($val,$q);
}
}
But echo and print work.
Where is the problem?
Not sure you've solved it already, but this is a solution that might help you on your way:
<?php
$arr = ['firstname' => 'John', 'lastname' => 'Dough', 'jobs' => ['job1' => 'writer', 'job2' => 'dad']];
$result = null; // the container for the search result
$key = 'job1'; // the key we are looking for
findKey($arr, $key, $result); // invoke the search function
/** print the result of our search - if key not found, outputs 'NULL' */
echo '<pre>';
var_dump($result); // output: string(6) "writer"
echo '<pre>';
/**
* #param array $arr the multidimensional array we are searching
* #param string $key the key we are looking for
* #param $result passed by reference - in case the key is found, this variable 'stores' the corresponding value.
*/
function findKey($arr = [], $key = '', &$result)
{
foreach ($arr as $key0 => $value0) {
if($key0 == $key) {
$result = $value0;
}
if(is_array($value0)) {
findKey($value0, $key, $result);
}
}
return false;
}
This question already has answers here:
How to access and manipulate multi-dimensional array by key names / path?
(10 answers)
Closed last year.
I have an unusual use-case I'm trying to code for. The goal is this: I want the customer to be able to provide a string, such as:
"cars.honda.civic = On"
Using this string, my code will set a value as follows:
$data['cars']['honda']['civic'] = 'On';
It's easy enough to tokenize the customer input as such:
$token = explode("=",$input);
$value = trim($token[1]);
$path = trim($token[0]);
$exploded_path = explode(".",$path);
But now, how do I use $exploded path to set the array without doing something nasty like an eval?
Use the reference operator to get the successive existing arrays:
$temp = &$data;
foreach($exploded as $key) {
$temp = &$temp[$key];
}
$temp = $value;
unset($temp);
Based on alexisdm's response :
/**
* Sets a value in a nested array based on path
* See https://stackoverflow.com/a/9628276/419887
*
* #param array $array The array to modify
* #param string $path The path in the array
* #param mixed $value The value to set
* #param string $delimiter The separator for the path
* #return The previous value
*/
function set_nested_array_value(&$array, $path, &$value, $delimiter = '/') {
$pathParts = explode($delimiter, $path);
$current = &$array;
foreach($pathParts as $key) {
$current = &$current[$key];
}
$backup = $current;
$current = $value;
return $backup;
}
Well tested and 100% working code. Set, get, unset values from an array using "parents". The parents can be either array('path', 'to', 'value') or a string path.to.value. Based on Drupal's code
/**
* #param array $array
* #param array|string $parents
* #param string $glue
* #return mixed
*/
function array_get_value(array &$array, $parents, $glue = '.')
{
if (!is_array($parents)) {
$parents = explode($glue, $parents);
}
$ref = &$array;
foreach ((array) $parents as $parent) {
if (is_array($ref) && array_key_exists($parent, $ref)) {
$ref = &$ref[$parent];
} else {
return null;
}
}
return $ref;
}
/**
* #param array $array
* #param array|string $parents
* #param mixed $value
* #param string $glue
*/
function array_set_value(array &$array, $parents, $value, $glue = '.')
{
if (!is_array($parents)) {
$parents = explode($glue, (string) $parents);
}
$ref = &$array;
foreach ($parents as $parent) {
if (isset($ref) && !is_array($ref)) {
$ref = array();
}
$ref = &$ref[$parent];
}
$ref = $value;
}
/**
* #param array $array
* #param array|string $parents
* #param string $glue
*/
function array_unset_value(&$array, $parents, $glue = '.')
{
if (!is_array($parents)) {
$parents = explode($glue, $parents);
}
$key = array_shift($parents);
if (empty($parents)) {
unset($array[$key]);
} else {
array_unset_value($array[$key], $parents);
}
}
$data = $value;
foreach (array_reverse($exploded_path) as $key) {
$data = array($key => $data);
}
Based on Ugo Méda's response :
This version
allows you to use it solely as a getter (leave the source array untouched)
fixes the fatal error issue if a non-array value is encountered (Cannot create references to/from string offsets nor overloaded objects)
no fatal error example
$a = ['foo'=>'not an array'];
arrayPath($a, ['foo','bar'], 'new value');
$a is now
array(
'foo' => array(
'bar' => 'new value',
),
)
Use as a getter
$val = arrayPath($a, ['foo','bar']); // returns 'new value' / $a remains the same
Set value to null
$v = null; // assign null to variable in order to pass by reference
$prevVal = arrayPath($a, ['foo','bar'], $v);
$prevVal is "new value"
$a is now
array(
'foo' => array(
'bar' => null,
),
)
/**
* set/return a nested array value
*
* #param array $array the array to modify
* #param array $path the path to the value
* #param mixed $value (optional) value to set
*
* #return mixed previous value
*/
function arrayPath(&$array, $path = array(), &$value = null)
{
$args = func_get_args();
$ref = &$array;
foreach ($path as $key) {
if (!is_array($ref)) {
$ref = array();
}
$ref = &$ref[$key];
}
$prev = $ref;
if (array_key_exists(2, $args)) {
// value param was passed -> we're setting
$ref = $value; // set the value
}
return $prev;
}
You need use Symfony PropertyPath
<?php
// ...
$person = array();
$accessor->setValue($person, '[first_name]', 'Wouter');
var_dump($accessor->getValue($person, '[first_name]')); // 'Wouter'
// or
// var_dump($person['first_name']); // 'Wouter'
This is exactly what this method is for:
Arr::set($array, $keys, $value);
It takes your $array where the element should be set, and accept $keys in dot separated format or array of subsequent keys.
So in your case you can achieve desired result simply by:
$data = Arr::set([], "cars.honda.civic", 'On');
// Which will be equivalent to
$data = [
'cars' => [
'honda' => [
'civic' => 'On',
],
],
];
What's more, $keys parameter can also accept creating auto index, so you can for example use it like this:
$data = Arr::set([], "cars.honda.civic.[]", 'On');
// In order to get
$data = [
'cars' => [
'honda' => [
'civic' => ['On'],
],
],
];
Can't you just do this
$exp = explode(".",$path);
$array[$exp[0]][$exp[1]][$exp[2]] = $value
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?
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);
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"
}
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;
}
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.
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
)
/**
* 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;
}
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.
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);
?>