14

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?

3
  • Technically, not really a PHP question, more of a Kohana question. Commented Oct 21, 2011 at 15:27
  • 6
    @MarcB AFAICT he's not looking for a Kohana solution but a pure PHP solution. Commented Oct 21, 2011 at 15:29
  • 1
    @MarcB Juhana is right, im not using Kohana, just reading the code to learn things. Commented Oct 21, 2011 at 15:44

7 Answers 7

41
function set_val(array &$arr, $path,$val)
{
   $loc = &$arr;
   foreach(explode('.', $path) as $step)
   {
     $loc = &$loc[$step];
   }
   return $loc = $val;
}
Sign up to request clarification or add additional context in comments.

7 Comments

Just a small edit to create entryes if some path are missing: pastebin.com/tKSeuuad
That would make it not work if part of the path already exists. The & will create path components that don't exist
youre right, dont know i got an error while i tryed the code the first time
If one of the steps (besides the last) exists and is a value other than null or an array, that might cause an error
I believe that strtok is better than explode in such cases!
|
13

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.

1 Comment

Sorry it doesnt work: pastebin.com/DYHw4Enb My goal is not to create an array with dot notation, but to edit it once created...
4

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]

Comments

4

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',
          ),
        )

    */

Comments

3

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):

1 Comment

Project doesn't exists anymore on GitHub
0

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;
    }
    

2 Comments

in my answer, when a sub path already exists, it is traversed correctly. You "updated" it to remove the by-reference traversal, causing the assignment to not actually modify the original array, which was the entire point
I just fixed it @hairraisin
-1

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
)

2 Comments

No eval please!
Not an argument

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.