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
Related
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);
?>
Was looking for a way to parse a string like private, max-age=86400 into an array like this:
[private] => TRUE
[max-age] => 86400
/**
* Parse the cache-control string into a key value array.
*
* #param string $cache_control
* The cache-control string.
*
* #return array
* Returns a key value array.
*/
function parse_cache_control($cache_control) {
$cache_control_array = explode(',', $cache_control);
$cache_control_array = array_map('trim', $cache_control_array);
$cache_control_parsed = array();
foreach ($cache_control_array as $value) {
if (strpos($value, '=') !== FALSE) {
$temp = array();
parse_str($value, $temp);
$cache_control_parsed += $temp;
}
else {
$cache_control_parsed[$value] = TRUE;
}
}
return $cache_control_parsed;
}
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 a function that updates fine with a single dimensional array, but with a multidimensional array (or nested array) it will not update. the document data, the BSONfield (acts as the find key), and the collection are imputed. Any Idea what i am doing wrong?
Public Static function updateDocument($collection, $BSONfield, $document){
$dbcollection = $db->selectCollection($collection);
$sdata = $document[$BSONfield];
$secureInnerDocument = array();
$secureDocument = array();
if($BSONfield == "_id"){
$sdata = new MongoID($sdata);
unset($document["_id"]);
}
$filter = array($BSONfield=>$sdata);
foreach ($document as $k => $v) {
if (is_array($v)) {
foreach ($v as $sk => $sv) {
$secureInnerDocument[$sk] = Security::secureQuery($sv);
}
$secureDocument[$k] = $secureInnerDocument;
}else{
$secureDocument[$k] = Security::secureQuery($v);
}
}
$dbcollection->update($filter,array('$set'=>$secureDocument));
$objid = (string) $secureDocument['_id'];
return $objid;
}
It translates fairly directly:
db.collection.update(
{fieldNameFilter:'filterValue'},
{$set: {'stringFieldName' : 'newValue'}}
);
Translates to:
$collection->update(
array('fieldNameFilter'=>'filterValue'),
array($set => array('stringFieldName'=>$value))
);
Then there are some flags for multi-row updates, etc. which I'm not showing here, but which are in the PHP and Mongo docs.
You might also want to look at: MongoDB - help with a PHP query
So after fiddling around with is my solution ended up being kind of janky, I deleted the Document then re-create it. Here is the code in case anyone is looking:
/**
* updates document in the collection.
* This function secures the data
*
* #return object ID
* #param string $collection The name of the collection
* #param string $BSONfield The $BSON Field you want to index by
* #param string $document The document contents as an array
*/
Public Static function updateDocument($collection, $BSONfield, $document){
$db = Database::dbConnect();
$collection = Security::secureQuery($collection);
$BSONfield = Security::secureQuery($BSONfield);
$dbcollection = $db->selectCollection($collection);
if(array_key_exists('_id', $document)){
$document["_id"] = new MongoID($document["_id"]);
}
Database::deleteDocument($collection, $BSONfield, $document);
$objid = Database::createDocument($collection, $document);
return $objid;
}
/**
* Deletes a document in the collection.
* This function secures the data
*
* #return Boolean True - if successfully deleted, False if document not found
* #param string $collection The name of the collection
* #param string $BSONfield The $BSON Field you want to index by
* #param string $document The document contents as an array
*/
Public Static function deleteDocument($collection, $BSONfield, $document){
$db = Database::dbConnect();
$collection = Security::secureQuery($collection);
$BSONfield = Security::secureQuery($BSONfield);
$exists = False;
$dbcollection = $db->selectCollection($collection);
$documentList = $dbcollection->find();
$sdata = $document[$BSONfield];
if($BSONfield == "_id"){
$sdata = new MongoID($sdata);
}
foreach ($documentList as $doc) {
$documentID = $doc[$BSONfield];
if ($documentID == $sdata){
$exists = True;
}
}
if ($exists){
$deleted = True;
$filter = array($BSONfield=>$sdata);
$dbcollection->remove($filter,true);
}else{
$deleted = False;
}
return $deleted;
}
/**
* Inserts document into the collection.
* This function secures the data
*
* #return object ID.
* #param string $collection The name of the collection
* #param string $document The document contents as an array
*/
Public Static function createDocument($collection, $document){
$db = Database::dbConnect();
$collection = Security::secureQuery($collection);
$dbcollection = $db->selectCollection($collection);
$secureDocument = array();
$secureInnerDocument = array();
foreach ($document as $k => $v) {
if (is_array($v)) {
foreach ($v as $sk => $sv) {
$secureInnerDocument[$sk] = Security::secureQuery($sv);
}
$secureDocument[$k] = $secureInnerDocument;
}else{
if ($k == '_id'){
$secureDocument[$k] = $v;
}else{
$secureDocument[$k] = Security::secureQuery($v);
}
}
}
$dbcollection->insert($secureDocument);
$objid = (string) $secureDocument['_id'];
return $objid;
}
and how i am securing all the data from injections:
/**
* Secures string to be inputed into a database.
*
* #return Retuns secure string
* #param String $string String to be secured
*/
Public Static Function secureQuery($string){
$secureString = strtr($string, array(
"'" => "0x27",
"\"" => "0x22",
"\\" => "0x5C",
"<" => "0x3C",
">" => "0x3E",
"=" => "0x3D",
"+" => "0x2B",
"&" => "0x26",
"{" => "0x7B",
"}" => "0x7D",
));
return $secureString;
}
/**
* Un-Secures string to be inputed into a database.
*
* #return Retuns unsecure string
* #param String $string String to be un-secured
*/
Public Static Function unsecureQuery($string){
$secureString = strtr($string, array(
"0x27" => "'",
"0x22" => "\"",
"0x5C" => "\\",
"0x3C" => "<",
"0x3E" => ">",
"0x3D" => "=",
"0x2B" => "+",
"0x26" => "&",
"0x7B" => "{",
"0x7D" => "}",
));
return $secureString;
}
enjoy!
You don't seem to be using the $set operator correctly. As per the MongoDB docs, you need to format your update document like so;
{ $set : { field : value } }
If you're running a $set on nested keys, you need to use dot notation to get to them. For example;
{ $set : { field.nest : 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);
?>