This question already has answers here:
How to access and manipulate multi-dimensional array by key names / path?
(10 answers)
Closed 3 years ago.
I have string:
Main.Sub.SubOfSub
And some kind of data, may be a string:
SuperData
How I can transform it all to this array above?
Array
(
[Main] => Array
(
[Sub] => Array
(
[SubOfSub] => SuperData
)
)
)
Thanks for help,
PK
Given the values
$key = "Main.Sub.SubOfSub";
$target = array();
$value = "SuperData";
Here's some code I have lying around that does what you need¹:
$path = explode('.', $key);
$root = &$target;
while(count($path) > 1) {
$branch = array_shift($path);
if (!isset($root[$branch])) {
$root[$branch] = array();
}
$root = &$root[$branch];
}
$root[$path[0]] = $value;
See it in action.
¹ Actually it does slightly more than that: it can be trivially encapsulated inside a function, and it is configurable on all three input values (you can pass in an array with existing values, and it will expand it as necessary).
Like Jon suggested (and being asking feedback for in chat), a reference/variable alias is helpful here to traverse the dynamic stack of keys. So the only thing needed is to iterate over all subkeys and finally set the value:
$rv = &$target;
foreach(explode('.', $key) as $pk)
{
$rv = &$rv[$pk];
}
$rv = $value;
unset($rv);
The reference makes it possible to use a stack instead of recursion which is generally more lean. Additionally this code prevents to overwrite existing elements in the $target array. Full example:
$key = "Main.Sub.SubOfSub";
$target = array('Main' => array('Sub2' => 'Test'));
$value = "SuperData";
$rv = &$target;
foreach(explode('.', $key) as $pk)
{
$rv = &$rv[$pk];
}
$rv = $value;
unset($rv);
var_dump($target);
Output:
array(1) {
["Main"]=>
array(2) {
["Sub2"]=>
string(4) "Test"
["Sub"]=>
array(1) {
["SubOfSub"]=>
string(9) "SuperData"
}
}
}
Demo
Related Question(s):
dynamic array key additions
How to group elements of array?
Related
I'm trying to implement my own serialization / var_dump style function in PHP. It seems impossible if there is the possibility of circular arrays (which there is).
In recent PHP versions, var_dump seems to detect circular arrays:
php > $a = array();
php > $a[] = &$a;
php > var_dump($a);
array(1) {
[0]=>
&array(1) {
[0]=>
*RECURSION*
}
}
How would I implement my own serialization type of method in PHP that can detect similarly? I can't just keep track of which arrays I've visited, because strict comparison of arrays in PHP returns true for different arrays that contain the same elements and comparing circular arrays causes a Fatal Error, anyways.
php > $b = array(1,2);
php > $c = array(1,2);
php > var_dump($b === $c);
bool(true)
php > $a = array();
php > $a[] = &$a;
php > var_dump($a === $a);
PHP Fatal error: Nesting level too deep - recursive dependency? in php shell code on line 1
I've looked for a way to find a unique id (pointer) for an array, but I can't find one. spl_object_hash only works on objects, not arrays. If I cast multiple different arrays to objects they all get the same spl_object_hash value (why?).
EDIT:
Calling print_r, var_dump, or serialize on each array and then using some mechanism to detect the presence of recursion as detected by those methods is an algorithmic complexity nightmare and will basically render any use too slow to be practical on large nested arrays.
ACCEPTED ANSWER:
I accepted the answer below that was the first to suggest temporarily altering the an array to see if it is indeed the same as another array. That answers the "how do I compare two arrays for identity?" from which recursion detection is trivial.
The isRecursiveArray(array) method below detects circular/recursive arrays. It keeps track of which arrays have been visited by temporarily adding an element containing a known object reference to the end of the array.
If you want help writing the serialization method, please update your topic question and provide a sample serialization format in your question.
function removeLastElementIfSame(array & $array, $reference) {
if(end($array) === $reference) {
unset($array[key($array)]);
}
}
function isRecursiveArrayIteration(array & $array, $reference) {
$last_element = end($array);
if($reference === $last_element) {
return true;
}
$array[] = $reference;
foreach($array as &$element) {
if(is_array($element)) {
if(isRecursiveArrayIteration($element, $reference)) {
removeLastElementIfSame($array, $reference);
return true;
}
}
}
removeLastElementIfSame($array, $reference);
return false;
}
function isRecursiveArray(array $array) {
$some_reference = new stdclass();
return isRecursiveArrayIteration($array, $some_reference);
}
$array = array('a','b','c');
var_dump(isRecursiveArray($array));
print_r($array);
$array = array('a','b','c');
$array[] = $array;
var_dump(isRecursiveArray($array));
print_r($array);
$array = array('a','b','c');
$array[] = &$array;
var_dump(isRecursiveArray($array));
print_r($array);
$array = array('a','b','c');
$array[] = &$array;
$array = array($array);
var_dump(isRecursiveArray($array));
print_r($array);
Funny method (I know it is stupid :)), but you can modify it and track the "path" to the recursive element. This is just an idea :) Based on the property of the serialized string, when recursion starts in will be the same as the string for the original array. As you can see - I tried it on many different variations and might be something is able to 'fool' it, but it 'detects' all listed recursions. And I did not try recursive arrays with objects.
$a = array('b1'=>'a1','b2'=>'a2','b4'=>'a3','b5'=>'R:1;}}}');
$a['a1'] = &$a;
$a['b6'] = &$a;
$a['b6'][] = array(1,2,&$a);
$b = serialize($a);
print_r($a);
function WalkArrayRecursive(&$array_name, &$temp){
if (is_array($array_name)){
foreach ($array_name as $k => &$v){
if (is_array($v)){
if (strpos($temp, preg_replace('#R:\d+;\}+$#', '',
serialize($v)))===0)
{
echo "\n Recursion detected at " . $k ."\n";
continue;
}
WalkArrayRecursive($v, $temp);
}
}
}
}
WalkArrayRecursive($a, $b);
regexp is for the situation when element with recursion is at the 'end' of the array. and, yes, this recursion is related to the whole array. It is possible to make recursion of the subelements, but it is too late for me to think about them. Somehow every element of the array should be checked for the recursion in its subelements. The same way, like above, through the output of the print_r function, or looking for specific record for recursion in serialized string (R:4;} something like this). And tracing should start from that element, comparing everything below by my script. All that is only if you want to detect where recursion starts, not just whether you have it or not.
ps: but the best thing should be, as I think, to write your own unserialize function from serailized string created by php itself.
My approach is to have a temp array that holds a copy of all objects that were already iterated. like this here:
// We use this to detect recursion.
global $recursion;
$recursion = [];
function dump( $data, $label, $level = 0 ) {
global $recursion;
// Some nice output for debugging/testing...
echo "\n";
echo str_repeat( " ", $level );
echo $label . " (" . gettype( $data ) . ") ";
// -- start of our recursion detection logic
if ( is_object( $data ) ) {
foreach ( $recursion as $done ) {
if ( $done === $data ) {
echo "*RECURSION*";
return;
}
}
// This is the key-line: Remember that we processed this item!
$recursion[] = $data;
}
// -- end of recursion check
if ( is_array( $data ) || is_object( $data ) ) {
foreach ( (array) $data as $key => $item ) {
dump( $item, $key, $level + 1 );
}
} else {
echo "= " . $data;
}
}
And here is some quick demo code to illustrate how it works:
$obj = new StdClass();
$obj->arr = [];
$obj->arr[] = 'Foo';
$obj->arr[] = $obj;
$obj->arr[] = 'Bar';
$obj->final = 12345;
$obj->a2 = $obj->arr;
dump( $obj, 'obj' );
This script will generate the following output:
obj (object)
arr (array)
0 (string) = Foo
1 (object) *RECURSION*
2 (string) = Bar
final (integer) = 12345
a2 (array)
0 (string) = Foo
1 (object) *RECURSION*
2 (string) = Bar
This is my approach. The key is to pass the array by reference to the recursive function simple_var_dump(), and use a tag (in this case "iterating_in_a_higher_level") to distinguish the arrays that are being iterated in a higher nesting level.
#!/usr/bin/php
<?php
function simple_var_dump(&$var, $depth = 0)
{
if (!is_array($var)) {
if (is_scalar($var)) {
return (string)$var;
} else {
return '?';
}
}
if (isset($var['__iterating_in_a_higher_level__'])) {
$r = 'array(' . (count($var)-1) . ')';
return $r . ' *RECURSION*';
}
$r = 'array(' . count($var) . ')';
$var['__iterating_in_a_higher_level__'] = true;
foreach ($var as $key => &$value) {
if ($key !== '__iterating_in_a_higher_level__') {
$r .= "\n" . str_repeat(' ', $depth + 1) . '[' . $key . '] => ' . simple_var_dump($value, $depth + 1);
}
}
unset($var['__iterating_in_a_higher_level__']);
return $r;
}
// example:
//
$a = [new stdClass(), &$a, 30, [40, [[&$a]]], [1, true, &$a], []];
echo simple_var_dump($a) . "\n";
Output:
array(6)
[0] => ?
[1] => array(6) *RECURSION*
[2] => 30
[3] => array(2)
[0] => 40
[1] => array(1)
[0] => array(1)
[0] => array(6) *RECURSION*
[4] => array(3)
[0] => 1
[1] => 1
[2] => array(6) *RECURSION*
[5] => array(0)
It's not elegant, but solves your problem (at least if you dont have someone using *RECURSION* as a value).
<?php
$a[] = &$a;
if(strpos(print_r($a,1),'*RECURSION*') !== FALSE) echo 1;
Let say I have an array like:
Array
(
[0] => Array
(
[Data] => Array
(
[id] => 1
[title] => Manager
[name] => John Smith
)
)
[1] => Array
(
[Data] => Array
(
[id] => 1
[title] => Clerk
[name] =>
(
[first] => Jane
[last] => Smith
)
)
)
)
I want to be able to build a function that I can pass a string to that will act as the array index path and return the appropriate array value without using eval(). Is that possible?
function($indexPath, $arrayToAccess)
{
// $indexPath would be something like [0]['Data']['name'] which would return
// "Manager" or it could be [1]['Data']['name']['first'] which would return
// "Jane" but the amount of array indexes that will be in the index path can
// change, so there might be 3 like the first example, or 4 like the second.
return $arrayToAccess[$indexPath] // <- obviously won't work
}
A Bit later, but... hope helps someone:
// $pathStr = "an:string:with:many:keys:as:path";
$paths = explode(":", $pathStr);
$itens = $myArray;
foreach($paths as $ndx){
$itens = $itens[$ndx];
}
Now itens is the part of the array you wanted to.
[]'s
Labs
you might use an array as path (from left to right), then a recursive function:
$indexes = {0, 'Data', 'name'};
function get_value($indexes, $arrayToAccess)
{
if(count($indexes) > 1)
return get_value(array_slice($indexes, 1), $arrayToAccess[$indexes[0]]);
else
return $arrayToAccess[$indexes[0]];
}
This is an old question but it has been referenced as this question comes up frequently.
There are recursive functions but I use a reference:
function array_nested_value($array, $path) {
$temp = &$array;
foreach($path as $key) {
$temp =& $temp[$key];
}
return $temp;
}
$path = array(0, 'Data', 'Name');
$value = array_nested_value($array, $path);
Try the following where $indexPath is formatted like a file path i.e.
'<array_key1>/<array_key2>/<array_key3>/...'.
function($indexPath, $arrayToAccess)
{
$explodedPath = explode('/', $indexPath);
$value =& $arrayToAccess;
foreach ($explodedPath as $key) {
$value =& $value[$key];
}
return $value;
}
e.g. using the data from the question, $indexPath = '1/Data/name/first' would return $value = Jane.
function($indexPath, $arrayToAccess)
{
eval('$return=$arrayToAccess'.$indexPath.';');
return $return;
}
You have to parse indexPath string. Chose some separator (for example "."), read text until "." that would be the first key, then read rest until next, that would be next key. Do that until no more dots.
You ken store key in array. Do foreach loop on this array to get seeked element.
Here is one way to get the job done, if string parsing is the way you want to go.
$data[0]["Data"]["stuff"] = "cake";
$path = "[0][\"Data\"]['stuff']";
function indexPath($path,$array){
preg_match_all("/\[['\"]*([a-z0-9_-]+)['\"]*\]/i",$path,$matches);
if(count($matches[1]) > 0) {
foreach ($matches[1] as $key) {
if (isset($array[$key])) {
$array = $array[$key];
} else {
return false;
}
}
} else {
return false;
}
return $array;
}
print_r(indexPath($path,$data));
A preg_match_all, cycling through the matched results would give you CLOSE to the result you wanted. You need to be careful with all of the strategies listed here for lost information. For instance, you have to devise some way to ensure that 55 stays as type int and isn't parsed as type string.
In addition to AbraCadaver:
function array_nested_value($array, $path) {
foreach($path as $key) {
$array = $array[$key];
}
return $array;
}
$path = array(0, 'Data', 'Name');
$value = array_nested_value($array, $path);
Possible use
function get_array_value($array=array(), $path=array()){
foreach($path as $key) {
if(isset($array[$key])){
$array=$array[$key];
}
else{
$array=NULL;
break;
}
}
return $array;
}
function custom_isset($array=array(), $path=array()){
$isset=true;
if(is_array($array)&&is_null(get_array_value($array, $path))){
$isset=false;
}
return $isset;
}
function is($array=array(), $path=array()){
$is=false;
if(is_array($array)){
$array_value=get_array_value($array, $path);
if(is_bool($array_value)){
$is=$array_value;
}
}
return $is;
}
Example
$status=[];
$status['updated']=true;
if(is($status,array('updated'))){
//do something...
}
Resources
https://gist.github.com/rafasashi/0d3ebadf08b8c2976a9d
If you already know the exact array element that you are pulling out why write a function to do it? What's wrong with just
$array[0]['data']['title']
If the parameter name of an URL contains squared brackets (no matter if they are url-encoded or not), any following character is ignored by PHP and is not made available to the script (e.g. via $_GET).
Example request:
...showget.php?xxx%5B1%5Dyyy=42
$_GET:
Array
(
[xxx] => Array
(
[1] => 42
)
)
As you can see, "yyy" didn't made it. ^^
(tested in PHP 5.3.28 & 5.5.10)
Does somebody know if such URLs are even syntactically valid?
Is this behaviour intended and documented (could not find anything) or should it rather be considered as a bug within PHP?
If intended: Can i change the respective behaviour by changing a special setting or so?
Thanks!
This is intended behaviour. As you saw in your example, PHP builds arrays from GET parameters if it can, that is, if it finds square brackets in the variable name. There's a FAQ entry showing how that can sometimes be useful.
In your case, PHP sees xxx[1]yyy=42 as xxx[1]=42 which becomes an array.
As far as I know, PHP's query string parsing can not be changed, but you could use $_SERVER['QUERY_STRING'] and parse that yourself.
[] in query key names is a hint to PHP that you want an array, e.g.
example.com?foo[]=bar&foo[]=baz
produces
$_GET = array(
'foo' => array('bar', 'baz')
);
This notation also lets you specify keys in the url:
example.com?foo[bar]=baz
$_GET = array(
'foo' => array('bar' => 'baz')
);
But once you get into this array notation, you're not permitted to have anything in the keyname AFTER the [] portion:
example.com?foo[bar]baz=qux
$_GET = array(
'foo' => array('bar' => 'qux')
);
Basically it's related to PHP syntax, where somethign like
$foo['bar']baz
would be a syntax error.
Came across this myself earlier too, and wrote a function to handle it from POST data, but it shouldn't take much to get it to use GET data instead. Such a large amount of code simply to account for the fact that PHP doesn't account for nested square brackets ;-)
/**
* Gets the _POST data with correct handling of nested brackets:
* "path[to][data[nested]]=value"
* "path"
* -> "to"
* -> "data[nested]" = value
* #return array
*/
function get_real_post() {
function set_nested_value(&$arr, &$keys, &$value) {
$key = array_shift($keys);
if (count($keys)) {
// Got deeper to go
if (!array_key_exists($key, $arr)) {
// Make sure we can get deeper if we've not hit this key before
$arr[$key] = array();
} elseif (!is_array($arr[$key])) {
// This should never be relevant for well formed input data
throw new Exception("Setting a value and an array with the same key: $key");
}
set_nested_value($arr[$key], $keys, $value);
} elseif (empty($key)) {
// Setting an Array
$arr[] = $value;
} else {
// Setting an Object
$arr[$key] = $value;
}
}
$input = array();
$parts = array();
$pairs = explode("&", file_get_contents("php://input"));
foreach ($pairs as $pair) {
$key_value = explode("=", $pair, 2);
preg_match_all("/([a-zA-Z0-9]*)(?:\[([^\[\]]*(?:(?R)[^\[\]]*)*)\])?/", urldecode($key_value[0]), $parts);
$keys = array($parts[1][0]);
if (!empty($parts[2][0])) {
array_pop($parts[2]); // Remove the blank one on the end
$keys = array_merge($keys, $parts[2]);
}
$value = urldecode($key_value[1]);
if ($value == "true") {
$value = true;
} else if ($value == "false") {
$value = false;
} else if (is_numeric($value)) {
if (strpos($value, ".") !== false) {
$num = floatval($value);
} else {
$num = intval($value);
}
if (strval($num) === $value) {
$value = $num;
}
}
set_nested_value($input, $keys, $value);
}
return $input;
}
I have a multidimensional array, here is a small excerpt:
Array (
[Albums] => Array (
[A Great Big World - Is There Anybody Out There] => Array(...),
[ATB - Contact] => Array(...),
)
[Pop] => Array (...)
)
And I have a dynamic path:
/albums/a_great_big_world_-_is_there_anybody_out_there
What would be the best way to retrieve the value of (in this example) $arr["albums"]["A Great Big World - Is There Anybody Out There"]?
Please note that it should be dynamic, since the nesting can go deeper than the 2 levels in this example.
EDIT
Here is the function I use to create a simple string for the URL:
function formatURL($url) {
return preg_replace('/__+/', '_', preg_replace('/[^a-z0-9_\s-]/', "", strtolower(str_replace(" ", "_", $url))));
}
$array = array(...);
$path = '/albums/a_great_big_world_-_is_there_anybody_out_there';
$value = $array;
foreach (explode('/', trim($path, '/')) as $key) {
if (isset($value[$key]) && is_array($value[$key])) {
$value = $value[$key];
} else {
throw new Exception("Path $path is invalid");
}
}
echo $value;
Example:
$arr = array(array("name"=>"Bob","species"=>"human","children"=>array(array("name"=>"Alice","age"=>10),array("name"=>"Jane","age"=>13)),array("name"=>"Sparky","species"=>"dog")));
print_r($arr);
array_walk_recursive($arr, function($v,$k) {
echo "key: $k\n";
});
The thing here is that I get only the last key, but I have no way to refer where I been, that is to store a particular key and change value after I left the function or change identical placed value in another identical array.
What I would have to get instead of string is an array that would have all keys leading to given value, for example [0,"children",1,"age"].
Edit:
This array is only example. I've asked if there is universal way to iterate nested array in PHP and get full location path not only last key. And I know that there is a way of doing this by creating nested loops reflecting structure of the array. But I repeat: I don't know the array structure in advance.
To solve your problem you will need recursion. The following code will do what you want, it will also find multiple paths if they exists:
$arr = array(
array(
"name"=>"Bob",
"species"=>"human",
"children"=>array(
array(
"name"=>"Alice",
"age"=>10
),
array(
"name"=>"Jane",
"age"=>13
)
),
array(
"name"=>"Sparky",
"species"=>"dog"
)
)
);
function getPaths($array, $search, &$paths, $currentPath = array()) {
foreach ($array as $key => $value) {
if (is_array($value)) {
$currentPath[] = $key;
if (true !== getPaths($value, $search, $paths, $currentPath)) {
array_pop($currentPath);
}
} else {
if ($search == $value) {
$currentPath[] = $key;
$paths[] = $currentPath;
return true;
}
}
}
}
$paths = array();
getPaths($arr, 13, $paths);
print_r($paths);