I made a small function to parse and get elements from a multidimensional array by a string written in a Unix-like path syntax.
function array_get($path, &$array) {
$keys = preg_split('/[\/\\\]+/', $path, null, PREG_SPLIT_NO_EMPTY);
$current = trim(array_shift($keys));
if (is_array($array) && array_key_exists($current, $array)) {
$path = implode("/", $keys);
if (empty($path)) {
// (Place the code here, see below)
return $array[$current];
}
return array_get($path, $array[$current]);
}
return false;
}
So if I got a simple array like this
$arr = array(
"A" => array(
"X" => array(),
"Y" => array(),
"Z" => array()
),
"B" => array(
"X" => array(),
"Y" => array(),
"Z" => array()
),
"C" => array(
"X" => array(),
"Y" => array(),
"Z" => array()
)
);
and I wish to fill it within some entries like these
$arr['A']['Z'][] = "foo";
$arr['A']['Z'][] = "bar";
I would do the same job using the following statements:
$var = array_get("A/Z", $arr);
$var[] = "foo";
$var[] = "bar";
But something went wrong.
If you try to run the code you will notice that going out of the local scope the references to the passed array will be lost.
If you wish to run a test, you can replace the placeholder comment line inside the function with these two code lines:
$array[$current][] = "foo";
$array[$current][] = "bar";
then you will see that the function would perform actually its own job.
Is there a way to maintain the references in output?
From the documentation, you can specify you want to return a reference by using the & character before the function name AND the function call.
<?php
function &foo(&$arr) {
return $arr[0];
}
$a = [[]];
$b = &foo($a);
$b[0] = 'bar';
print_r($a); /* outputs [ [ 'bar' ] ] */
You can return references.
But I find your approach really cumbersome, and it will lead to misbehaviours / maintainability / readability issues very soon.
Related
Given a PHP array that looks like:
[
'foo' => 1,
'bar[0]' => 6,
'bar[1]' => 7,
'bar[2]' => 8,
'baz' => 'anything',
... and so on
]
I want to convert the "implied" nesting into real arrays, while leaving the rest untouched, to result in:
[
'foo' => 1,
'bar' => [6, 7, 8],
'baz' => 'anything',
]
I've searched the php docs but can't find a utility for this. I'm sure I could write a function to do this, but it feels like reinventing the wheel. Surely such a function already exists?
You can use array_walk() and preg_match to see if the key should be an "array". We can then pass in our final array by reference to allow us to edit it.
For example
<?php
$a = [
'foo' => 1,
'bar[0]' => 6,
'bar[1]' => 7,
'bar[2]' => 8,
'baz' => 'anything',
];
$end = [];
array_walk($a, function($val, $key) use(&$end) {
//See if the key is something like "bar[1]"
if( preg_match("/^([a-z]+)\[[0-9]+\]$/", $key, $match) ) {
//See if "bar" key exists in our final array, if not create it.
if( array_key_exists($match[1], $end) == FALSE ) {
return $end[$match[1]] = array($val);
}
//Add value to array we created above
return $end[$match[1]][] = $val;
}
//It's just a normal key, so just add it to our final array
return $end[$key] = $val;
});
print_r($end);
https://eval.in/315998
just playing around. See comments for explanation of code.
/*
our source array
*/
$a = array(
'foo' => 1,
'bar[0]' => 6,
'bar[1]' => 7,
'bar[2]' => 8,
'baz' => 'anything'
);
// an array declared to hold all variable names present in array.
$vars = array();
/*
http://php.net/manual/en/function.extract.php
extract all values from the array with keys are variable names. Keys like
bar[0] do not make sense to extract function so it ignores them.
*/
extract($a);
/*
Now that we've got all variables we possibly could using extract(), we
traverse the source array to create the $bar array ourselves.
*/
foreach($a as $k => $v) {
/*
if key contains a [
this check could be rigorous, but I leave that to the production code developer
*/
if(strstr($k, '[')) {
/*
replace the [number] part from key to get the array name, i.e., "bar"
*/
$arr_name = preg_replace('/\[\d+\]/', '', $k);
/*
using variable variables feature (http://php.net/manual/en/language.variables.variable.php)
check if we've created the array already. if not, create now. and
record the variable name in $vars array for future reference
*/
if(!is_array($$arr_name)) {
$$arr_name = array();
$vars[] = $arr_name;
}
/*
formulate and eval() (http://php.net/manual/en/function.eval.php)
a statement that inserts current $v into our created array
eval is evil so do some rigorous testing before using it
*/
eval('$' . $k . '=' . $v . ';');
}
else{
//otherwise just record the variable.
$vars[] = $k;
}
}
/* $vars holds names of all variables you got from stream */
var_dump($vars);
/* display the variables */
var_dump($foo, $bar, $baz);
/* almost forgot, http://php.net/manual/en/function.var-dump.php */
I did this code:
index.php:
$series = array(
"a" => array(
"b" => array(
"FOLD", "more_arrays.php"
),
"b2" => array(
)
)
);
function pre($a) { print "<pre>"; print_r($a); print "</pre>"; }
$string = "a,,,b";
$all_directions = explode(",,,", $string);
$all_directions = array_map("trim", $all_directions);
$b = ""; $g = 0;
foreach($all_directions as $v)
{
$b .= "['".str_replace(array("[", "]", "'", "\""), null, $v)."']";
$g++;
}
#eval('$where = $series'.$b.';');
if(isset($where[0]) && $where[0] == "FOLD")
{
// a[series], b[series], c[new_array]
require_once("./more_folders/".$where[1]);
print $g;
}
for($i = 0; $i <= sizeof($where); $i++)
{
}
pre($where);
more_array.php:
$series_in = array(
"c" => array(
"d" => array(
"bla" => array(),
"hey" => array(),
"ha" => array()
),
"d2" => array(
)
),
"c2" => array(
)
)
At $string I define which "folder" I want to see, for example if I write $string = "a"; it will show all the arrays inside "a".
key = the name of the folder, value = the subfolders inside the folder and those array.
Now: Because it's going to be a huge array, I want to separate it to many arrays.
If you see at the code, $series[a][b] direct lead to another array.
Now if I do $string = "a,,,b"; I want to see: "c" and "c2"
and if I do $string = "a,,,b,,,c"; I want to see: "d", "d2"
and if I do $string = "a,,,b,,,c,,,d"; I want to see all inside d ( "bla", "hey", "ha" ..)
How can I do this?
I'll bite...
You seem to have most of the parts. Basically you need to put them together in a loop.
You've $string and $series. Then you split $string into your $all_directions. Loop thru $all_directions, each time diving down into the array $series = $series[$all_directions[$i]]; When you've done the last $all_directions return $series (but watch for running out of $series, return null, or false if you're sure that would be an error).
The only other thing is any time $series[$all_directions[$i]] is the special "FOLD" entry then first load the file and assign it on-the-fly something like
include ...;
$series[$all_directions[$i]] = $series_in;
You don't want and don't need eval() and the loop is better using for because you need to check "FOLD" in the key (I'd also say use a recursive function but you said the array can be very big so it might hurt performance).
I am having trouble figuring out a way to simply parse a string input and find the correct location within a multidimensional array.
I am hoping for one or two lines to do this, as the solutions I have seen rely on long (10-20 line) loops.
Given the following code (note that the nesting could, in theory, be of any arbitrary depth):
function get($string)
{
$vars = array(
'one' => array(
'one-one' => "hello",
'one-two' => "goodbye"
),
'two' => array(
'two-one' => "foo",
'two-two' => "bar"
)
);
return $vars[$string]; //this syntax isn't required, just here to give an idea
}
get("two['two-two']"); //desired output: "bar". Actual output: null
Is there a simple use of built-in functions or something else easy that would recreate my desired output?
Considering $vars being your variables you would like to get one['one-one'] or two['two-two']['more'] from (Demo):
$vars = function($str) use ($vars)
{
$c = function($v, $w) {return $w ? $v[$w] : $v;};
return array_reduce(preg_split('~\[\'|\'\]~', $str), $c, $vars);
};
echo $vars("one['one-one']"); # hello
echo $vars("two['two-two']['more']"); # tea-time!
This is lexing the string into key tokens and then traverse the $vars array on the keyed values while the $vars array has been turned into a function.
Older Stuff:
Overload the array with a function that just eval's:
$vars = array(
'one' => array(
'one-one' => "hello",
'one-two' => "goodbye"
),
'two' => array(
'two-one' => "foo",
'two-two' => "bar"
)
);
$vars = function($str) use ($vars)
{
return eval('return $vars'.$str.';');
};
echo $vars("['one']['one-two']"); # goodbye
If you're not a fan of eval, change the implementation:
$vars = function($str) use ($vars)
{
$r = preg_match_all('~\[\'([a-z-]+)\']~', $str, $keys);
$var = $vars;
foreach($keys[1] as $key)
$var = $var[$key];
return $var;
};
echo $vars("['one']['one-two']"); # goodbye
How about
$vars = array(
'one' => array(
'one-one' => "hello",
'one-two' => "goodbye"
),
'two' => array(
'two-one' => "foo",
'two-two' => "bar"
)
);
function get( $string, $vars )
{
$keys = explode( '][', substr( $string, 1, -1 ) );
foreach( $keys as $key ) {
$vars = $vars[$key];
}
return $vars;
}
echo get( '[two][two-one]', $vars );
For one, you've not got a $var in your get() function. $var was defined outside the function, and PHP scoping rules do not make "higher" vars visible in lower scopes unless explictly made global in the lower scope:
function get($string) {
global $vars;
eval('$x = $vars' . $string);
return $x;
}
get("['two']['two-two']");
might work, but this isn't tested, and using eval is almost always a very bad idea.
Kohana has a nice Config class which alows something like this:
echo Config::get("two.two-two");
You can check it out here: http://kohanaframework.org/3.1/guide/api/Config
I have a class which stores values with a multi-level associative array:
I need to add a way to access and modify nested values. Here is a working solution for my problem, but it is rather slow. Is there a better way of doing this?
Note: The use of get / set functions is not mandatory, but there needs to be an efficient way to define a default value.
class Demo {
protected $_values = array();
function __construct(array $values) {
$this->_values = $values;
}
public function get($name, $default = null) {
$token = strtok($name, '.#');
$node = $this->_values;
while ($token !== false) {
if (!isset($node[$token]))
return $default;
$node = $node[$token];
$token = strtok('.#');
}
return $node;
}
public function set($name, $value) {
$next_token = strtok($name, '.#');
$node = &$this->_values;
while ($next_token !== false) {
$token = $next_token;
$next_token = strtok('.#');
if ($next_token === false) {
$node[ $token ] = $value;
break;
}
else if (!isset($node[ $token ]))
$node[ $token ] = array();
$node = &$node[ $token ];
}
unset($node);
}
}
Which would be used as follows:
$test = new Demo(array(
'simple' => 27,
'general' => array(
0 => array(
'something' => 'Hello World!',
'message' => 'Another message',
'special' => array(
'number' => 27
)
),
1 => array(
'something' => 'Hello World! #2',
'message' => 'Another message #2'
),
)
));
$simple = $test->get('simple'); // === 27
$general_0_something = $test->get('general#0.something'); // === 'Hello World!'
$general_0_special_number = $test->get('general#0.special.number'); === 27
Note: 'general.0.something' is the same as 'general#0.something', the alternative punctuation is for the purpose of clarity.
Well, the question was interesting enough that I couldn't resist tinkering a bit more. :-)
So, here are my conclusions. Your implementation is probably the most straightforward and clear. And it's working, so I wouldn't really bother about searching for another solution. In fact, how much calls are you gonna get in the end? Is the difference in performance worth the trouble (I mean between "super ultra blazingly fast" and "almost half as fast")?
Put aside though, if performance is really an issue (getting thousands of calls), then there's a way to reduce the execution time if you repetitively lookup the array.
In your version the greatest burden falls on string operations in your get function. Everything that touches string manipulation is doomed to fail in this context. And that was indeed the case with all my initial attempts at solving this problem.
It's hard not to touch strings if we want such a syntax, but we can at least limit how much string operations we do.
If you create a hash map (hash table) so that you can flatten your multidimensional array to a one level deep structure, then most of the computations done are a one time expense. It pays off, because this way you can almost directly lookup your values by the string provided in your get call.
I've come up with something roughly like this:
<?php
class Demo {
protected $_values = array();
protected $_valuesByHash = array();
function createHashMap(&$array, $path = null) {
foreach ($array as $key => &$value) {
if (is_array($value)) {
$this->createHashMap($value, $path.$key.'.');
} else {
$this->_valuesByHash[$path.$key] =& $value;
}
}
}
function __construct(array $values) {
$this->_values = $values;
$this->createHashMap($this->_values);
// Check that references indeed work
// $this->_values['general'][0]['special']['number'] = 28;
// print_r($this->_values);
// print_r($this->_valuesByHash);
// $this->_valuesByHash['general.0.special.number'] = 29;
// print_r($this->_values);
// print_r($this->_valuesByHash);
}
public function get($hash, $default = null) {
return isset($this->_valuesByHash[$hash]) ? $this->_valuesByHash[$hash] : $default;
}
}
$test = new Demo(array(
'simple' => 27,
'general' => array(
'0' => array(
'something' => 'Hello World!',
'message' => 'Another message',
'special' => array(
'number' => 27
)
),
'1' => array(
'something' => 'Hello World! #2',
'message' => 'Another message #2'
),
)
));
$start = microtime(true);
for ($i = 0; $i < 10000; ++$i) {
$simple = $test->get('simple', 'default');
$general_0_something = $test->get('general.0.something', 'default');
$general_0_special_number = $test->get('general.0.special.number', 'default');
}
$stop = microtime(true);
echo $stop-$start;
?>
The setter is not yet implemented, and you would have to modify it for alternative syntax (# separator), but I think it conveys the idea.
At least on my testbed it takes half the time to execute this compared to the original implementation. Still raw array access is faster, but the difference in my case is around 30-40%. At the moment that was the best I could achieve. I hope that your actual case is not big enough that I've hit some memory constraints on the way. :-)
Ok, my first approached missed the goal I was aiming for. Here is the solution to using native PHP array syntax (at least for access) and still being able to set a default value.
Update: Added missing functionality for get/set and on the fly converting.
By the way, this is not an approach to take if you are optimizing for performance. This is perhaps 20 times slower than regular array access.
class Demo extends ArrayObject {
protected $_default;
public function __construct($array,$default = null) {
parent::__construct($array);
$this->_default = $default;
}
public function offsetGet($index) {
if (!parent::offsetExists($index)) return $this->_default;
$ret = parent::offsetGet($index);
if ($ret && is_array($ret)) {
parent::offsetSet($index, $this->newObject($ret));
return parent::offsetGet($index);
}
return $ret;
}
protected function newObject(array $array=null) {
return new self($array,$this->_default);
}
}
Init
$test = new Demo(array(
'general' => array(
0 => array(
'something' => 'Hello World!'
)
)
),'Default Value');
Result
$something = $test['general'][0]['something']; // 'Hello World!'
$notfound = $test['general'][0]['notfound']; // 'Default Value'
You're looking for something like that? Essentially the get() method uses references to descend into the $values array and breaks out of the method if a requirement could not be met.
class Demo {
protected $_values = array();
public function __construct(array $values) {
$this->_values = $values;
}
public function get($name, $default = null) {
$parts = preg_split('/[#.]/', $name);
if (!is_array($parts) || empty($parts)) {
return null;
}
$value = &$this->_values;
foreach ($parts as $p) {
if (array_key_exists($p, $value)) {
$value = &$value[$p];
} else {
return null;
}
}
return $value;
}
/**
* setter missing
*/
}
$test = new Demo(array(
'simple' => 2,
'general' => array(
0 => array(
'something' => 'Hello World!',
'message' => 'Another message',
'special' => array(
'number' => 4
)
),
1 => array(
'something' => 'Hello World! #2',
'message' => 'Another message #2'
)
)
));
$v = $test->get('simple');
var_dump($v);
$v = $test->get('general');
var_dump($v);
$v = $test->get('general.0');
var_dump($v);
$v = $test->get('general#0');
var_dump($v);
$v = $test->get('general.0.something');
var_dump($v);
$v = $test->get('general#0.something');
var_dump($v);
$v = $test->get('general.0.message');
var_dump($v);
$v = $test->get('general#0.message');
var_dump($v);
$v = $test->get('general.0.special');
var_dump($v);
$v = $test->get('general#0.special');
var_dump($v);
$v = $test->get('general.0.special.number');
var_dump($v);
$v = $test->get('general#0.special.number');
var_dump($v);
$v = $test->get('general.1');
var_dump($v);
$v = $test->get('general#1');
var_dump($v);
$v = $test->get('general.1.something');
var_dump($v);
$v = $test->get('general#1.something');
var_dump($v);
$v = $test->get('general.1.message');
var_dump($v);
$v = $test->get('general#1.message');
var_dump($v);
This is how multidimensional array work in general in PHP:
$data = array(
'general' => array(
0 => array(
'something' => 'Hello World!'
)
)
);
To receive Hello World:
echo $data['general'][0]['something'];
I suspect I'm doing something stupid here, but I'm confused by what seems like a simple problem with SPL:
How do I modified the contents of an array (the values in this example), using a RecursiveArrayIterator / RecursiveIteratorIterator?
Using the follow test code, I can alter the value within the loop using getInnerIterator() and offsetSet(), and dump out the modified array while I'm within the loop.
But when I leave the loop and dump the array from the iterator, it's back to the original values. What's happening?
$aNestedArray = array();
$aNestedArray[101] = range(100, 1000, 100);
$aNestedArray[201] = range(300, 25, -25);
$aNestedArray[301] = range(500, 0, -50);
$cArray = new ArrayObject($aNestedArray);
$cRecursiveIter = new RecursiveIteratorIterator(new RecursiveArrayIterator($cArray), RecursiveIteratorIterator::LEAVES_ONLY);
// Zero any array elements under 200
while ($cRecursiveIter->valid())
{
if ($cRecursiveIter->current() < 200)
{
$cInnerIter = $cRecursiveIter->getInnerIterator();
// $cInnerIter is a RecursiveArrayIterator
$cInnerIter->offsetSet($cInnerIter->key(), 0);
}
// This returns the modified array as expected, with elements progressively being zeroed
print_r($cRecursiveIter->getArrayCopy());
$cRecursiveIter->next();
}
$aNestedArray = $cRecursiveIter->getArrayCopy();
// But this returns the original array. Eh??
print_r($aNestedArray);
It seems that values in plain arrays aren't modifiable because they can't be passed by reference to the constructor of ArrayIterator (RecursiveArrayIterator inherits its offset*() methods from this class, see SPL Reference). So all calls to offsetSet() work on a copy of the array.
I guess they chose to avoid call-by-reference because it doesn't make much sense in an object-oriented environment (i. e. when passing instances of ArrayObject which should be the default case).
Some more code to illustrate this:
$a = array();
// Values inside of ArrayObject instances will be changed correctly, values
// inside of plain arrays won't
$a[] = array(new ArrayObject(range(100, 200, 100)),
new ArrayObject(range(200, 100, -100)),
range(100, 200, 100));
$a[] = new ArrayObject(range(225, 75, -75));
// The array has to be
// - converted to an ArrayObject or
// - returned via $it->getArrayCopy()
// in order for this field to get handled properly
$a[] = 199;
// These values won't be modified in any case
$a[] = range(100, 200, 50);
// Comment this line for testing
$a = new ArrayObject($a);
$it = new RecursiveIteratorIterator(new RecursiveArrayIterator($a));
foreach ($it as $k => $v) {
// getDepth() returns the current iterator nesting level
echo $it->getDepth() . ': ' . $it->current();
if ($v < 200) {
echo "\ttrue";
// This line is equal to:
// $it->getSubIterator($it->getDepth())->offsetSet($k, 0);
$it->getInnerIterator()->offsetSet($k, 0);
}
echo ($it->current() == 0) ? "\tchanged" : '';
echo "\n";
}
// In this context, there's no real point in using getArrayCopy() as it only
// copies the topmost nesting level. It should be more obvious to work with $a
// itself
print_r($a);
//print_r($it->getArrayCopy());
You need to call getSubIterator at the current depth, use offsetSet at that depth, and do the same for all depths going back up the tree.
This is really useful for doing unlimited level array merge and replacements, on arrays or values within arrays. Unfortunately, array_walk_recursive will NOT work in this case as that function only visits leaf nodes.. so the 'replace_this_array' key in $array below will never be visited.
As an example, to replace all values within an array unknown levels deep, but only those that contain a certain key, you would do the following:
$array = [
'test' => 'value',
'level_one' => [
'level_two' => [
'level_three' => [
'replace_this_array' => [
'special_key' => 'replacement_value',
'key_one' => 'testing',
'key_two' => 'value',
'four' => 'another value'
]
],
'ordinary_key' => 'value'
]
]
];
$arrayIterator = new \RecursiveArrayIterator($array);
$completeIterator = new \RecursiveIteratorIterator($arrayIterator, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($completeIterator as $key => $value) {
if (is_array($value) && array_key_exists('special_key', $value)) {
// Here we replace ALL keys with the same value from 'special_key'
$replaced = array_fill(0, count($value), $value['special_key']);
$value = array_combine(array_keys($value), $replaced);
// Add a new key?
$value['new_key'] = 'new value';
// Get the current depth and traverse back up the tree, saving the modifications
$currentDepth = $completeIterator->getDepth();
for ($subDepth = $currentDepth; $subDepth >= 0; $subDepth--) {
// Get the current level iterator
$subIterator = $completeIterator->getSubIterator($subDepth);
// If we are on the level we want to change, use the replacements ($value) other wise set the key to the parent iterators value
$subIterator->offsetSet($subIterator->key(), ($subDepth === $currentDepth ? $value : $completeIterator->getSubIterator(($subDepth+1))->getArrayCopy()));
}
}
}
return $completeIterator->getArrayCopy();
// return:
$array = [
'test' => 'value',
'level_one' => [
'level_two' => [
'level_three' => [
'replace_this_array' => [
'special_key' => 'replacement_value',
'key_one' => 'replacement_value',
'key_two' => 'replacement_value',
'four' => 'replacement_value',
'new_key' => 'new value'
]
],
'ordinary_key' => 'value'
]
]
];
Not using the Iterator classes (which seem to be copying data on the RecursiveArrayIterator::beginChildren() instead of passing by reference.)
You can use the following to achieve what you want
function drop_200(&$v) { if($v < 200) { $v = 0; } }
$aNestedArray = array();
$aNestedArray[101] = range(100, 1000, 100);
$aNestedArray[201] = range(300, 25, -25);
$aNestedArray[301] = range(500, 0, -50);
array_walk_recursive ($aNestedArray, 'drop_200');
print_r($aNestedArray);
or use create_function() instead of creating the drop_200 function, but your mileage may vary with the create_function and memory usage.
Looks like getInnerIterator creates a copy of the sub-iterator.
Maybe there is a different method? (stay tuned..)
Update: after hacking at it for a while, and pulling in 3 other engineers, it doesn't look like PHP gives you a way to alter the values of the subIterator.
You can always use the old stand by:
<?php
// Easy to read, if you don't mind references (and runs 3x slower in my tests)
foreach($aNestedArray as &$subArray) {
foreach($subArray as &$val) {
if ($val < 200) {
$val = 0;
}
}
}
?>
OR
<?php
// Harder to read, but avoids references and is faster.
$outherKeys = array_keys($aNestedArray);
foreach($outherKeys as $outerKey) {
$innerKeys = array_keys($aNestedArray[$outerKey]);
foreach($innerKeys as $innerKey) {
if ($aNestedArray[$outerKey][$innerKey] < 200) {
$aNestedArray[$outerKey][$innerKey] = 0;
}
}
}
?>
Convert the array to an object first and it works as expected..
$array = [
'one' => 'One',
'two' => 'Two',
'three' => [
'four' => 'Four',
'five' => [
'six' => 'Six',
'seven' => 'Seven'
]
]
];
// Convert to object (using whatever method you want)
$array = json_decode(json_encode($array));
$iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($array));
foreach($iterator as $key => $value) {
$iterator->getInnerIterator()->offsetSet($key, strtoupper($value));
}
var_dump($iterator->getArrayCopy());
I know this doesn't answer your question directly, but it's not a good practice to modify the object under iteration while iterating over it.
Could it come down to passing by reference vs passing by value?
For example try changing:
$cArray = new ArrayObject($aNestedArray);
to:
$cArray = new ArrayObject(&$aNestedArray);