Looking into Kohana documentation, i found this really usefull function that they use to get values from a multidimensional array using a dot notation, for example:
$foo = array('bar' => array('color' => 'green', 'size' => 'M'));
$value = path($foo, 'bar.color', NULL , '.');
// $value now is 'green'
Im wondering if there is a way to set the an array value in the same way:
set_value($foo, 'bar.color', 'black');
The only way i found to do that is re-building the array notation ($array['bar']['color']) and then set the value.. using eval
.
Any idea to avoid eval?
function set_val(array &$arr, $path,$val)
{
$loc = &$arr;
foreach(explode('.', $path) as $step)
{
$loc = &$loc[$step];
}
return $loc = $val;
}
Sure it's possible.
The code
function set_value(&$root, $compositeKey, $value) {
$keys = explode('.', $compositeKey);
while(count($keys) > 1) {
$key = array_shift($keys);
if(!isset($root[$key])) {
$root[$key] = array();
}
$root = &$root[$key];
}
$key = reset($keys);
$root[$key] = $value;
}
How to use it
$foo = array();
set_value($foo, 'bar.color', 'black');
print_r($foo);
Outputs
Array
(
[bar] => Array
(
[color] => black
)
)
See it in action.
Look at https://gist.github.com/elfet/4713488
$dn = new DotNotation(['bar'=>['baz'=>['foo'=>true]]]);
$value = $dn->get('bar.baz.foo'); // $value == true
$dn->set('bar.baz.foo', false); // ['foo'=>false]
$dn->add('bar.baz', ['boo'=>true]); // ['foo'=>false,'boo'=>true]
That way you can set the following values more than once to the same variable.
You can make these two ways (by static variable and reference variable):
<?php
function static_dot_notation($string, $value)
{
static $return;
$token = strtok($string, '.');
$ref =& $return;
while($token !== false)
{
$ref =& $ref[$token];
$token = strtok('.');
}
$ref = $value;
return $return;
}
$test = static_dot_notation('A.1', 'A ONE');
$test = static_dot_notation('A.2', 'A TWO');
$test = static_dot_notation('B.C1', 'C ONE');
$test = static_dot_notation('B.C2', 'C TWO');
$test = static_dot_notation('B.C.D', 'D ONE');
var_export($test);
/**
array (
'A' =>
array (
1 => 'A ONE',
2 => 'A TWO',
),
'B' =>
array (
'C1' => 'C ONE',
'C2' => 'C TWO',
'C' =>
array (
'D' => 'D ONE',
),
),
*/
function reference_dot_notation($string, $value, &$array)
{
static $return;
$token = strtok($string, '.');
$ref =& $return;
while($token !== false)
{
$ref =& $ref[$token];
$token = strtok('.');
}
$ref = $value;
$array = $return;
}
reference_dot_notation('person.name', 'Wallace', $test2);
reference_dot_notation('person.lastname', 'Maxters', $test2);
var_export($test2);
/**
array (
'person' =>
array (
'name' => 'Wallace',
'lastname' => 'Maxters',
),
)
*/
I created a small class just for this!
http://github.com/projectmeta/Stingray
$stingray = new StingRay();
//To Get value
$stingray->get($array, 'this.that.someother'):
//To Set value
$stingray->get($array, 'this.that.someother', $newValue):
Updated @hair resins' answer to cater for:
- When a sub-path already exists, or
When a sub-path is not an array
function set_val(array &$arr, $path,$val)
{
$loc = &$arr;
$path = explode('.', $path);
foreach($path as $step)
{
if ( ! isset($loc[$step]) OR ! is_array($loc[$step]))
$loc = &$loc[$step];
}
return $loc = $val;
}
None of the examples here worked for me, so I came up with a solution using eval() (read about the risks here, but if you don't use user data, it shouldn't be much of an issue). The if-clause in the set-method allows you to push your item onto a new or existing array at that location ($location[] = $item).
class ArrayDot {
public static function get(array &$array, string $path, string $delimiter = '.') {
return eval("return ".self::getLocationCode($array, $path, $delimiter).";");
}
public static function set(array &$array, string $path, $item, string $delimiter = '.') : void {
//if the last character is a delimiter, allow pushing onto a new or existing array
$add = substr($path, -1) == $delimiter ? '[]': '';
eval(self::getLocationCode($array, $path, $delimiter).$add." = \$item;");
}
public static function unset(array &$array, $path, string $delimiter = '.') : void {
if (is_array($path)) {
foreach($path as $part) {
self::unset($array, $part, $delimiter);
}
}
else {
eval('unset('.self::getLocationCode($array, $path, $delimiter).');');
}
}
public static function isSet(array &$array, $path, string $delimiter = '.') : bool {
if (is_array($path)) {
foreach($path as $part) {
if (!self::isSet($array, $part, $delimiter)) {
return false;
}
}
return true;
}
return eval("return isset(".self::getLocationCode($array, $path, $delimiter).");");
}
private static function getLocationCode(array &$array, string $path, string $delimiter) : string {
$path = rtrim($path, $delimiter); //Trim trailing delimiters
$escapedPathParts = array_map(function ($s) { return str_replace('\'', '\\\'', $s); }, explode($delimiter, $path));
return "\$array['".implode("']['", $escapedPathParts)."']";
}
}
Example usage:
echo '<pre>';
$array = [];
ArrayDot::set($array, 'one.two.three.', 'one.two.three.');
ArrayDot::set($array, 'one.two.three.four.', 'one.two.three.four.');
ArrayDot::set($array, 'one.two.three.four.', 'one.two.three.four. again');
ArrayDot::set($array, 'one.two.three.five.', 'one.two.three.five.');
ArrayDot::set($array, 'one.two.three.direct set', 'one.two.three.direct set');
print_r($array);
echo "\n";
echo "one.two.three.direct set: ".print_r(ArrayDot::get($array, 'one.two.three.direct set'), true)."\n";
echo "one.two.three.four: ".print_r(ArrayDot::get($array, 'one.two.three.four'), true)."\n";
Output:
Array
(
[one] => Array
(
[two] => Array
(
[three] => Array
(
[0] => one.two.three.
[four] => Array
(
[0] => one.two.three.four.
[1] => one.two.three.four. again
)
[five] => Array
(
[0] => one.two.three.five.
)
[direct set] => one.two.three.direct set
)
)
)
)
one.two.three.direct set: one.two.three.direct set
one.two.three.four: Array
(
[0] => one.two.three.four.
[1] => one.two.three.four. again
)