I'm trying to build an associative array in PHP dynamically, and not quite getting my strategy right. Basically, I want to insert a value at a certain depth in the array structure, for instance:
$array['first']['second']['third'] = $val;
Now, the thing is, I'm not sure if that depth is available, and if it isn't, I want to create the keys (and arrays) for each level, and finally insert the value at the correct level.
Since I'm doing this quite a lot in my code, I grew tired of doing a whole bunch of "array_key_exists", so I wanted to do a function that builds the array for me, given a list of the level keys. Any help on a good strategy for this is appreciated. I'm sure there is a pretty simple way, I'm just not getting it...
php doesn't blame you if you do it just so
$array['first']['second']['third'] = $val;
print_r($array);
if you don't want your keys to be hard coded, here's a flexible solution
/// locate or create element by $path and set its value to $value
/// $path is either an array of keys, or a delimited string
function array_set(&$a, $path, $value) {
if(!is_array($path))
$path = explode($path[0], substr($path, 1));
$key = array_pop($path);
foreach($path as $k) {
if(!isset($a[$k]))
$a[$k] = array();
$a = &$a[$k];
}
$a[$key ? $key : count($a)] = $value;
}
// example:
$x = array();
array_set($x, "/foo/bar/baz", 123);
array_set($x, "/foo/bar/quux", 456);
array_set($x, array('foo', 'bah'), 789);
Create a function like:
function insert_into(&$array, array $keys, $value) {
$last = array_pop($keys);
foreach($keys as $key) {
if(!array_key_exists($key, $array) ||
array_key_exists($key, $array) && !is_array($array[$key])) {
$array[$key] = array();
}
$array = &$array[$key];
}
$array[$last] = $value;
}
Usage:
$a = array();
insert_into($a, array('a', 'b', 'c'), 1);
print_r($a);
Ouput:
Array
(
[a] => Array
(
[b] => Array
(
[c] => 1
)
)
)
That's tricky, you'd need to work with references (or with recursion, but I
chose references here):
# Provide as many arguments as you like:
# createNestedArray($array, 'key1', 'key2', etc.)
function createNestedArray(&$array) {
$arrayCopy = &$array;
$args = func_get_args();
array_shift($args);
while (($key = array_shift($args)) !== false) {
$arrayCopy[$key] = array();
$arrayCopy = &$arrayCopy[$key];
}
}
<?php
function setElements(&$a, array $path = [], $values = [])
{
if (!is_array($path)) {
$path = explode($path[0], substr($path, 1));
}
$path = "[ '" . join("' ][ '", $path) . "' ]";
$code =<<<CODE
if(!isset(\$a{$path})){
\$a{$path} = [];
}
return \$a{$path}[] = \$values;
CODE;
return eval($code);
}
$a = [];
setElements($a, [1,2], 'xxx');
setElements($a, [1,2,3], 233);
setElements($a, [1,2,4], 'AAA');
setElements($a, [1,2,3,4], 555);
print_r($a);
Output
Array
(
[1] => Array
(
[2] => Array
(
[0] => xxx
[3] => Array
(
[0] => 233
[4] => Array
(
[0] => 555
)
)
[4] => Array
(
[0] => AAA
)
)
)
)
You should check it here http://sandbox.onlinephpfunctions.com/
Related
I want to concat values of array with same key
Example:
[0] => Array
(
[0] => A
[1] => XYZ
)
[1] => Array
(
[0] => B
[1] => ABC
)
[2] => Array
(
[0] => A
[1] => LMN
)
[3] => Array
(
[0] => B
[1] => PQR
)
)
Expected output:
[0] => Array
(
[0] => A
[1] => XYZ,LMN
)
[1] => Array
(
[0] => B
[1] => ABC,PQR
)
)
A simple solution uses the PHP function array_reduce():
// The input array you posted in the question
$input = array(
array('A', 'XYZ'),
array('B', 'ABC'),
array('A', 'LMN'),
array('B', 'PQR'),
);
// Reduce the array to a new array that contains the data aggregated as you need
$output = array_reduce(
// Process each $item from $input using a callback function
$input,
// The callback function processes $item; the partial result is $carry
function (array $carry, array $item) {
// Extract the key into a variable
$key = $item[0];
// If the key was encountered before
// then a partial entry already exists in $carry
if (isset($carry[$key])) {
// Append the desired value to the existing entry
$carry[$key][1] .= ','.$item[1];
} else {
// Create a new entry in $carry (copy $item to key $key for quick retrieval)
$carry[$key] = $item;
}
// Return the updated $carry
return $carry;
},
// Start with an empty array (it is known as $carry in the callback function)
array()
);
// $output contains the array you need
Try this:
$final = array();
foreach ($array_items as $item)
{
$key = $item[0];
$found_index = -1;
for ($i=0; $i<count($final); $i++)
{
if ($key == $final[$i][0])
{
$found_index = $i;
break;
}
}
if ($found_index == -1)
{
$final_item = array();
$final_item[0] = $key;
$final_item[1] = $item[1];
$final[] = $final_item;
}
else
{
$final[$found_index][1] .= ",".$item[1];
}
}
We create a new array $final, and loop through your old array $array_items. For each item, we see if there is already an item in $final that has the same [0] index. If it doesn't exist, we create it and add the initial string to the [1] index. If it does exist, we just have to add the string onto the end of the [1] index.
Try it, substituting $array_items for whatever your array is called, let me know if it works.
Check my solution. It should work fine. I hope it will help you much.
$result = $passed_keys = $extended_arr = [];
foreach ($arr as $k => $value) {
for($i = $k + 1; $i < count($arr); $i++){
if ( $value[0] == $arr[$i][0] ){ // compare each array with rest subsequent arrays
$key_name = $value[0];
if (!array_key_exists($key_name, $result)){
$result[$key_name] = $value[1] .",". $arr[$i][1];
} else {
if (!in_array($i, $passed_keys[$key_name])) {
$result[$key_name] .= ",". $arr[$i][1];
}
}
$passed_keys[$key_name][] = $i; // memorizing keys that were passed
}
}
}
array_walk($result, function($v, $k) use(&$extended_arr){
$extended_arr[] = [$k, $v];
});
The result is in $extended_arr
My solution, creates a custom key which makes identifying the letter much easier. This removes the need to continuously iterate through each array, which can become a major resources hog.
<?php
$inital_array = array(
array('A','XYZ'),
array('B','ABC'),
array('A','LMN'),
array('B','PQR')
);
$concat_array = array();
foreach($inital_array as $a){
$key = $a[0];
if( !isset($concat_array[$key]) ){
$concat_array[$key] = array($key,'');
}
$concat_array[$key][1] .= (empty($concat_array[$key][1]) ? '' : ',').$a[1];
}
$concat_array = array_values($concat_array);
echo '<pre>',print_r($concat_array),'</pre>';
Say for example I have the following array:
$h = array (
"app" => array (
"level1" => array (
"level2" => array (
"level3" =>3
),
"level4" => array (
"level5" => 2
)
)
)
);
What I wish to do is to create a string for every single sub-array found in here. For example, using the array above, the output would be:
Array
(
[0] => Array
(
[app.level1.level2.level3] => 3
)
[1] => Array
(
[app.level1.level4.level5] => 2
)
)
As you can see, each sub-array is concatenated with a '.' to represent the fact there is a child array with a value assigned given by the last node. Of course the only thing I can think of is to create a recursive function that could handle this, though this is where I'm having some trouble here. Here's what I started working on:
public static function buildString($array, $string ="") {
foreach($array as $h => $k) {
if(is_array($k)) {
$string .= $h.".";
return self::buildString($k, $string);
} else {
$string .= $h;
$j[] = array (
$string => $k
);
return $j;
}
}
}
Inputting the array given above within this method, I successfully get the first iteration:
Array
(
[0] => Array
(
[app.level1.level2.level3] => 3
)
)
And this is where I am at the moment and cannot seem to figure out how to do the rest of the array, or any size array for that matter.
Any hints//remarks would be appreciated.
You cannot return inside the foreach loop, you need to aggregate all of the recursive/non-recursive results and bubble them up. Something like this:
public static function buildString($array, $string ="") {
$j = array();
foreach($array as $h => $k) {
if(is_array($k)) {
$string .= $h.".";
$j = array_merge($j, self::buildString($k, $string));
} else {
$string .= $h;
$j[] = array (
$string => $k
);
}
}
return $j;
}
you can use array_walk() as a method:
$h = array (
"app" => array (
"level1" => array (
"level2" => array (
"level3" =>3
),
"level4" => array (
"level5" => 2
)
)
)
);
$results = array();
function get_strings($item, $key, $old_key = null) {
global $results;
if(is_array($item)) {
array_walk($item, 'get_strings', $old_key . $key . ".");
} else {
$results[$old_key . $key] = $item;
}
}
array_walk($h, 'get_strings');
print_r($results); //returns Array ( [app.level1.level2.level3] => 3 [app.level1.level4.level5] => 2 )
array_walk() documentation: http://php.net/manual/en/function.array-walk.php
Although I must give credit to #kennypu, I've made some slight modifications to the answer in order to contain the code in one function without the use of global or any other variables within the class (keeping it all in one method).
public static function buildString($array, $delimeter = '.') {
$results = array();
$func = function($item, $key, $old_key = NULL) use (&$func, &$results, $delimeter) {
if(is_array($item)) {
array_walk($item, $func, $old_key . $key . $delimeter);
} else {
$results[$old_key . $key] = $item;
}
};
array_walk($array, $func);
return $results;
}
In essence I created an anonymous function that uses parameters from the parent with the use of the key word use. Although not much documentation can be found for use some examples are shown here: Anonymous Functions.
Say, I have data in a tree structure implemented as an array of arrays to an arbitrary depth, something like
print_r($my_array);
Array
(
[id] => 123
[value] => Hello, World!
[child] => Array
(
[name] => Foo
[bar] => baz
)
[otherchild] => Array
(
[status] => fubar
[list] => Array
(
[one] => 1
[two] => 3
)
)
[sanity] => unchecked
)
Now, using a single string as key I would like to address a node at an arbitrary depth, Let's say I have a key something like this:
$key = 'otherchild|list|two';
Using this key I want to be able to address the value stored in
$my_array['otherchild']['list']['two']
Obviously I can explode('|', $key) to get an array of keys, and shifting values off of this and using those to address sub-arrays makes it easy to obtain the value I'm looking for, something like
$value = $my_array;
$keys = explode('|', $key);
while ($k = array_shift($keys)) {
if (isset($value[$k])) {
$value = $value[$k];
} else {
// handle failure
}
} // Here, if all keys exist, $value holds value of addressed node
But I'm stuck trying to update values in a generic way, ie without having to resort to something like
$keys = explode('|', $key);
if (count($keys) == 1) {
$my_array[$keys[0]] = $new_value;
} else if (count($keys) == 2) {
$my_array[$keys[0]][$keys[1]] = $new_value;
} else if ...
Any ideas?
function setAt(array & $a, $key, $value)
{
$keys = explode('|', $key);
// Start with the root node (the array itself)
$node = & $a;
// Walk the tree, create nodes as needed
foreach ($keys as $k) {
// Create node if it does not exist
if (! isset($node[$k])) {
$node[$k] = array();
}
// Walk to the node
$node = & $node[$k];
}
// Position found; store the value
$node = $value;
}
// Test
$array = array();
// Add new values
setAt($array, 'some|key', 'value1');
setAt($array, 'some|otherkey', 'val2');
setAt($array, 'key3', 'value3');
print_r($array);
// Overwrite existing values
setAt($array, 'some|key', 'new-value');
print_r($array);
setAt($array, 'some', 'thing');
print_r($array);
If you're looking for a short answer, you can also use eval().
$elem = "\$array['" . str_replace("|", "']['", $key) . "']";
$val = eval("return isset($elem) ? $elem : null;");
I am trying to remove eval() from my code.
Currently I use:
eval( "$mkt_data" . $branch . " = $xyz" );
to assign $xyz (which is also a multidimensional array) to the specific branch of $mkt_data.
$branch can be 1, 2 or 3 levels deep, i.e.
$branch = "['mkt1']['offer']['bid']";
or
$branch = "['status']";
is there a clever way to do this?
You can use a function like this:
function set(&$ary, $keys, $value) {
$last = array_pop($keys);
foreach($keys as $k) {
if(!isset($ary[$k]))
$ary[$k] = array();
$ary = &$ary[$k];
}
$ary[$last] = $value;
}
Example:
$data = array();
set($data, array('foo', 'bar', 'baz'), 42);
print_r($data);
Result:
Array
(
[foo] => Array
(
[bar] => Array
(
[baz] => 42
)
)
)
I have a collection of keys in this massive flat single array I would like to basically expand that array into a multidimensional one organized by keys - here is an example:
'invoice/products/data/item1'
'invoice/products/data/item2'
'invoice/products/data/item2'
=>
'invoice'=>'products'=>array('item1','item2','item3')
how can I do this - the length of the above strings are variable...
Thanks!
$src = array(
'invoice/products/data/item1',
'invoice/products/data/item2',
'invoice/products/data/item2',
'foo/bar/baz',
'aaa/bbb'
);
function rsplit(&$v, $w)
{
list($first, $tail) = explode('/', $w, 2);
if(empty($tail))
{
$v[] = $first;
return $v;
}
$v[$first] = rsplit($v[$first], $tail);
return $v;
}
$result = array_reduce($src, "rsplit");
print_r($result);
Output is:
Array (
[invoice] => Array
(
[products] => Array
(
[data] => Array
(
[0] => item1
[1] => item2
[2] => item2
)
)
)
[foo] => Array
(
[bar] => Array
(
[0] => baz
)
)
[aaa] => Array
(
[0] => bbb
)
)
Something along these lines: (Didn't test it though!) Works now ;)
$data = array();
$current = &$data;
foreach($keys as $value) {
$parts = explode("/", $value);
$parts_count = count($parts);
foreach($parts as $i => $part) {
if(!array_key_exists($part, $current)) {
if($i == $parts_count - 1) {
$current[] = $part;
}
else {
$current[$part] = array();
$current = &$current[$part];
}
}
else {
$current = &$current[$part];
}
}
$current = &$data;
}
$keys beeing the flat array.
Although it's not clear from your question how the "/" separated strings will map to an array, the basic approach will probably be something like this:
$result = array();
$k1 = $k2 = '';
ksort($yourData); // This is the key (!)
foreach ($yourData as $k => $v) {
// Use if / else if / else if to watch for new sub arrays and change
// $k1, $k2 accordingly
$result[$k1][$k2] = $v;
}
This approach uses the ksort to ensure that keys at the same "level" appear together, like this:
'invoice/products/data1/item1'
'invoice/products/data1/item2'
'invoice/products/data2/item3'
'invoice/products2/data3/item4'
'invoice/products2/data3/item5'
Notice how the ksort corresponds to the key grouping you're aiming for.