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'];
Related
Okay, so I need to dynamically dig into a JSON structure with PHP and I don't even know if it is possible.
So, let's say that my JSON is stored ad the variable $data:
$data = {
'actions':{
'bla': 'value_actionBla',
'blo': 'value_actionBlo',
}
}
So, to access the value of value_actionsBla, I just do $data['actions']['bla']. Simple enough.
My JSON is dynamically generated, and the next time, it is like this:
$data = {
'actions':{
'bla': 'value_actionBla',
'blo': 'value_actionBlo',
'bli':{
'new': 'value_new'
}
}
}
Once again, to get the new_value, I do: $data['actions']['bli']['new'].
I guess you see the issue.
If I need to dig two levels, then I need to write $data['first_level']['second_level'], with three, it will be $data['first_level']['second_level']['third_level'] and so on ...
Is there any way to perform such actions dynamically? (given I know the keys)
EDIT_0: Here is an example of how I do it so far (in a not dynamic way, with 2 levels)
// For example, assert that 'value_actionsBla' == $data['actions']['bla']
foreach($data as $expected => $value) {
$this->assertEquals($expected, $data[$value[0]][$value[1]]);
}
EDIT_1
I have made a recursive function to do it, based on the solution of #Matei Mihai:
private function _isValueWhereItSupposedToBe($supposedPlace, $value, $data){
foreach ($supposedPlace as $index => $item) {
if(($data = $data[$item]) == $value)
return true;
if(is_array($item))
$this->_isValueWhereItSupposedToBe($item, $value, $data);
}
return false;
}
public function testValue(){
$searched = 'Found';
$data = array(
'actions' => array(
'abort' => '/abort',
'next' => '/next'
),
'form' => array(
'title' => 'Found'
)
);
$this->assertTrue($this->_isValueWhereItSupposedToBe(array('form', 'title'), $searched, $data));
}
You can use a recursive function:
function array_search_by_key_recursive($needle, $haystack)
{
foreach ($haystack as $key => $value) {
if ($key === $needle) {
return $value;
}
if (is_array($value) && ($result = array_search_by_key_recursive($needle, $value)) !== false) {
return $result;
}
}
return false;
}
$arr = ['test' => 'test', 'test1' => ['test2' => 'test2']];
var_dump(array_search_by_key_recursive('test2', $arr));
The result is string(5) "test2"
You could use a function like this to traverse down an array recursively (given you know all the keys for the value you want to access!):
function array_get_nested_value($data, array $keys) {
if (empty($keys)) {
return $data;
}
$current = array_shift($keys);
if (!is_array($data) || !isset($data[$current])) {
// key does not exist or $data does not contain an array
// you could also throw an exception here
return null;
}
return array_get_nested_value($data[$current], $keys);
}
Use it like this:
$array = [
'test1' => [
'foo' => [
'hello' => 123
]
],
'test2' => 'bar'
];
array_get_nested_value($array, ['test1', 'foo', 'hello']); // will return 123
I have an GLOBAL array that keeps all the configurations, it looks like this:
$_SOME_ARRAY = array(
'some_setting' => array(
'some_value' => '1',
'other' => 'value'
),
'something_else' => 1,
);
How can I delete keys from this array, using some function like:
deleteFromArray('some_setting/other')
I have tried different things, but can't seem to find a way, to delete it without manually calling unset($_SOME_ARRAY['some_setting']['other'])
EDIT
I have tried working on with it. The only solution I see so far, is by "rebuilding" the original array, by looping through each value and verify. The progress:
public static function delete($path) {
global $_EDU_SETUP;
$exploded_path = explode('/', $path);
$newConfig = array();
$delete = false;
foreach($exploded_path as $bit) {
if(!$delete) {
$loop = $_EDU_SETUP;
} else {
$loop = $delete;
}
foreach($loop as $key => $value) {
if($key == $bit) {
echo 'found first: ' . $key . '<br />'; // debugging
if(!$delete) {
$delete = $_EDU_SETUP[$key];
} else {
$delete = $delete[$key];
}
} else {
$newConfig[$key] = $value;
}
}
}
$_EDU_SETUP = $newConfig;
}
The array could look like this:
$array = array(
'a' => array(
'a',
'b',
'c'
),
'b' => array(
'a',
'b',
'c' => array(
'a',
'b',
'c' => array(
'a'
),
),
)
);
And to delete $array['b']['c'] you would write Config::delete('b/c'); - BUT: It deletes whole B. It is only supposed to delete C.
Any ideas?
This what you can do, assuming the array has 2 levels of data.
$_SOME_ARRAY = array(
'some_setting' => array(
'some_value' => '1',
'other' => 'value'
),
'something_else' => 1,
);
function deleteFromArray($param){
global $_SOME_ARRAY ;
$param_values = explode("/",$param);
if(count($param_values) == 2 ){
unset($_SOME_ARRAY[$param_values[0]][$param_values[1]]);
}else{
unset($_SOME_ARRAY[$param_values[0]]);
}
}
deleteFromArray('some_setting/other');
print_r($_SOME_ARRAY);
You can modify the function to add more strict rules by checking if the key exists before doing unset using the function array_key_exists()
how do you like this ?
$_SESSION = $_SOME_ARRAY; // Btw it should be session from beginning...
function deleteFromArray($string)
{
$array = explode("/",$sting);
foreach($array as $arrA)
{
foreach($array as $arrB)
{
unset($_SESSION[$arrA][$arrB]);
}
}
}
now you could delete more than one entry like
deleteFromArray('some_setting/some_value/a_other_value')
but take care of using dim1array names in dim2array...
of corse you could add more foreach or make a recursiv function out of it to get deep in the array
do you want to delete particular array using a unique index(like a primary id)?, i would use a for loop to look for that particular index then delete that array...E.g delete array where the index = 1 , pls check above
foreach ($_SOME_ARRAY as $a => $key)//get all child arrays of '$_SOME_ARRAY '
{
foreach($Key as $b => $key2)//get values of the child arrays
{
if($Key[0] == 1)// if the index at[0] equals 1
{
unset($arr[$a][$b]); //delete that array
}
}
}
Imagine the following multi-dimensional array:
$a = array(
'key' => 'hello',
'children' => array(
array(
'key' => 'sub-1'
),
array(
'key' => 'sub-2',
'children' => array(
array(
'key' => 'sub-sub-1'
)
)
)
)
);
I require a function that recursively runs through such an array and then finally returns a chain of all the values of a certain sub-key, using a glue string.
function collectKeyChain(array $array, $key, $parentKey, $glue){
foreach($array as $k => $v){
if(is_array($v[$parentKey]))
$children=self::collectKeyChain($v[$parentKey], $key, $parentKey, $glue, $out);
$chain[]=$glue . implode($glue, $children);
}
return $chain;
}
Called this way:
collectValueChain($a, 'key', 'children', '/');
Should then return this:
array(
'hello',
'hello/sub-1',
'hello/sub-2',
'hello/sub-2/sub-sub-1'
)
Unfortunately my brain seems completely unable to perform the task of "nested thinking". The code provided in the function above doesn't work, simply because it makes no sense. I can either use the recursive function to return an array or a string. But in the final output i require an array. On the other hand i need to chain the elements together.
That's the dilemma. And the only solution that came up in my head was using another parameter, that is passed by reference, which is an array that is being filled with the results.
Like this:
collectValueChain($a, 'key', 'children', '/', $arrayToBeFilledWithResults);
But i was unable to make even this work without getting into using multiple functions.
Perhaps it just cannot be done more easily, but i would still like to find out.
Try this one:
function collectKeyChain(array $array, $key, $parentKey, $glue) {
$return = array();
foreach ($array as $k => $v) {
if ($k == $key) {
$base = $v;
$return[] = $base;
} elseif ($k == $parentKey && is_array($v)) {
foreach ($v as $_v) {
$children = collectKeyChain($_v, $key, $parentKey, $glue);
foreach ($children as $child) {
$return[] = $base . $glue . $child;
}
}
}
}
return $return;
}
Note that if this is to be a static method in a class you have to add self:: to the recursive method call.
A more simple version, without lots of foreach. Consider the second approach:
collectValueChain($a, 'key', 'children', '/', $arrayToBeFilledWithResults);
I do this:
function collectValueChain($a, $keyname, $parent, $glue, &$rtn, $pre="") {
$_pre = "";
if ($a[$keyname]) {
$rtn[] = $_pre = $pre.$glue.$a[$keyname];
}
if ($a[$parent]) {
if(is_array($a[$parent])) {
foreach($a[$parent] as $c)
collectValueChain($c, $keyname, $parent, $glue, $rtn, $_pre );
} else {
collectValueChain(a[$parent], $keyname, $parent, $glue, $rtn, $_pre );
}
}
$qtd = count($rtn);
return $rtn[-1];
}
This is sort of a general implementation question. If I have an arbitrarily deep array, and I do not know before hand what the keys will be, what is the best way to access the values at specific paths of the associative array? For example, given the array:
array(
'great-grandparent' = array(
'grandparent' = array(
'parent' = array(
'child' = 'value';
),
'parent2' = 'value';
),
'grandparent2' = 'value';
)
);
Whats the best way to access the value at $array['great-grandparent']['grandparent']['parent']['child'] keeping in mind that I don't know the keys beforehand. I have used eval to construct the above syntax as a string with variable names and then eval'd the string to get the data. But eval is slow and I was hoping for something faster. Something like $class->getConfigValue('great-grandparent/grandparent/'.$parent.'/child'); that would return 'value'
Example of Eval Code
public function getValue($path, $withAttributes=false) {
$path = explode('/', $path);
$rs = '$r = $this->_data[\'config\']';
foreach ($path as $attr) {
$rs .= '[\'' . $attr . '\']';
}
$rs .= ';';
$r = null;
#eval($rs);
if($withAttributes === false) {
$r = $this->_removeAttributes($r);
}
return $r;
}
I don't know about the potential speed but you don't need to use eval to do a search like that :
$conf = array(
'great-grandparent' => array(
'grandparent' => array(
'parent' => array(
'child' => 'value searched'
),
'parent2' => 'value'
),
'grandparent2' => 'value'
)
);
$path = 'great-grandparent/grandparent/parent/child';
$path = explode('/', $path);
$result = $conf;
while(count($path) > 0) {
$part = array_shift($path);
if (is_array($result) && array_key_exists($part, $result)) {
$result = $result[$part];
} else {
$result = null;
break;
}
}
echo $result;
Here we go, my solution:
$tree = array(
'great-grandparent' => array(
'grandparent' => array(
'parent' => array(
'child' => 'value1'
),
'parent2' => 'value2'
),
'grandparent2' => 'value3'
)
);
$pathParts = explode('/','great-grandparent/grandparent/parent/child');
$pathParts = array_reverse($pathParts);
echo retrieveValueForPath($tree, $pathParts);
function retrieveValueForPath($node, $pathParts) {
foreach($node as $key => $value) {
if(($key == $pathParts[count($pathParts)-1]) && (count($pathParts)==1)) {
return $value;
}
if($key == $pathParts[count($pathParts)-1]) {
array_pop($pathParts);
}
if(is_array($value)) {
$result = retrieveValueForPath($value, $pathParts);
}
}
return $result;
}
I'm building a small template system and i'm looking for a way to invoke multidimensional associative arrays using dots. For example:
$animals = array(
'four-legged' => array (
'cute' => 'no',
'ugly' => 'no',
'smart' => array('best' => 'dog','worst' => 'willy')
),
'123' => '456',
'abc' => 'def'
);
Then, in my template, if I wanted to show 'dog', I would put:
{a.four-legged.smart.best}
Well, given a string with four-legged.smart.worst:
function getElementFromPath(array $array, $path) {
$parts = explode('.', $path);
$tmp = $array;
foreach ($parts as $part) {
if (!isset($tmp[$part])) {
return ''; //Path is invalid
} else {
$tmp = $tmp[$part];
}
}
return $tmp; //If we reached this far, $tmp has the result of the path
}
So you can call:
$foo = getElementFromPath($array, 'four-legged.smart.worst');
echo $foo; // willy
And if you want to write elements, it's not much harder (you just need to use references, and a few checks to default the values if the path doesn't exist)...:
function setElementFromPath(array &$array, $path, $value) {
$parts = explode('.', $path);
$tmp =& $array;
foreach ($parts as $part) {
if (!isset($tmp[$part]) || !is_array($tmp[$part])) {
$tmp[$part] = array();
}
$tmp =& $tmp[$part];
}
$tmp = $value;
}
Edit: Since this is in a template system, it may be worth while "compiling" the array down to a single dimension once, rather than traversing it each time (for performance reasons)...
function compileWithDots(array $array) {
$newArray = array();
foreach ($array as $key => $value) {
if (is_array($value)) {
$tmpArray = compileWithDots($value);
foreach ($tmpArray as $tmpKey => $tmpValue) {
$newArray[$key . '.' . $tmpKey] = $tmpValue;
}
} else {
$newArray[$key] = $value;
}
}
return $newArray;
}
So that would convert:
$animals = array(
'four-legged' => array (
'cute' => 'no',
'ugly' => 'no',
'smart' => array(
'best' => 'dog',
'worst' => 'willy'
)
),
'123' => '456',
'abc' => 'def'
);
Into
array(
'four-legged.cute' => 'no',
'four-legged.ugly' => 'no',
'four-legged.smart.best' => 'dog',
'four-legged.smart.worst' => 'willy',
'123' => '456',
'abc' => 'def',
);
Then your lookup just becomes $value = isset($compiledArray[$path]) ? $compiledArray[$path] : ''; instead of $value = getElementFromPath($array, $path);
It trades pre-computing for inline speed (speed within the loop)...