25

I've to implement a setter in PHP, that allows me to specify the key, or sub key, of an array (the target), passing the name as a dot-separated-keys value.

Given the following code:

$arr = array('a' => 1,
             'b' => array(
                 'y' => 2,
                 'x' => array('z' => 5, 'w' => 'abc')
             ),
             'c' => null);

$key = 'b.x.z';
$path = explode('.', $key);

From the value of $key I want to reach the value 5 of $arr['b']['x']['z'].

Now, given a variable value of $key and a different $arr value (with different deepness).

How can I set the value of the element referred by by $key?

For the getter get() I wrote this code:

public static function get($name, $default = null)
{
    $setting_path = explode('.', $name);
    $val = $this->settings;

    foreach ($setting_path as $key) {
        if(array_key_exists($key, $val)) {
            $val = $val[$key];
        } else {
            $val = $default;
            break;
        }
    }
    return $val;
}

To write a setter is more difficult because I succeed in reaching the right element (from the $key), but I am not able to set the value in the original array and I don't know how to specify the keys all at once.

Should I use some kind of backtracking? Or can I avoid it?

Community
  • 1
  • 1
Kamafeather
  • 8,663
  • 14
  • 69
  • 99
  • 1
    Does this answer your question? [use strings to access (potentially large) multidimensional arrays](https://stackoverflow.com/questions/7003559/use-strings-to-access-potentially-large-multidimensional-arrays) – Brombomb Feb 10 '21 at 17:56

10 Answers10

34

Assuming $path is already an array via explode (or add to the function), then you can use references. You need to add in some error checking in case of invalid $path etc. (think isset):

$key = 'b.x.z';
$path = explode('.', $key);

Getter

function get($path, $array) {
    //$path = explode('.', $path); //if needed
    $temp =& $array;

    foreach($path as $key) {
        $temp =& $temp[$key];
    }
    return $temp;
}

$value = get($path, $arr); //returns NULL if the path doesn't exist

Setter / Creator

This combination will set a value in an existing array or create the array if you pass one that has not yet been defined. Make sure to define $array to be passed by reference &$array:

function set($path, &$array=array(), $value=null) {
    //$path = explode('.', $path); //if needed
    $temp =& $array;

    foreach($path as $key) {
        $temp =& $temp[$key];
    }
    $temp = $value;
}

set($path, $arr);
//or
set($path, $arr, 'some value');

Unsetter

This will unset the final key in the path:

function unsetter($path, &$array) {
    //$path = explode('.', $path); //if needed
    $temp =& $array;

    foreach($path as $key) {
        if(!is_array($temp[$key])) {
            unset($temp[$key]);
        } else {
            $temp =& $temp[$key];
        }
    }
}
unsetter($path, $arr);

*The original answer had some limited functions that I will leave in case they are of use to someone:

Setter

Make sure to define $array to be passed by reference &$array:

function set(&$array, $path, $value) {
    //$path = explode('.', $path); //if needed
    $temp =& $array;

    foreach($path as $key) {
        $temp =& $temp[$key];
    }
    $temp = $value;
}

set($arr, $path, 'some value');

Or if you want to return the updated array (because I'm bored):

function set($array, $path, $value) {
    //$path = explode('.', $path); //if needed
    $temp =& $array;

    foreach($path as $key) {
        $temp =& $temp[$key];
    }
    $temp = $value;

    return $array;
}

$arr = set($arr, $path, 'some value');

Creator

If you wan't to create the array and optionally set the value:

function create($path, $value=null) {
    //$path = explode('.', $path); //if needed
    foreach(array_reverse($path) as $key) {
        $value = array($key => $value);
    }
    return $value;
}    

$arr = create($path);    
//or
$arr = create($path, 'some value');

For Fun

Constructs and evaluates something like $array['b']['x']['z'] given a string b.x.z:

function get($array, $path) {
    //$path = explode('.', $path); //if needed
    $path = "['" . implode("']['", $path) . "']";
    eval("\$result = \$array{$path};");

    return $result;
}

Sets something like $array['b']['x']['z'] = 'some value';:

function set(&$array, $path, $value) {
    //$path = explode('.', $path); //if needed
    $path = "['" . implode("']['", $path) . "']";
    eval("\$array{$path} = $value;");
}

Unsets something like $array['b']['x']['z']:

function unsetter(&$array, $path) {
    //$path = explode('.', $path); //if needed
    $path = "['" . implode("']['", $path) . "']";
    eval("unset(\$array{$path});");
}
AbraCadaver
  • 78,200
  • 7
  • 66
  • 87
  • You know? I wrote the same code by myself, but I trashed it without even testing it because I thought that reassigning every time the value to the same variable would have changed also the array values in the middle (before reaching the desired one), provoking unpredictable side effects! Instead it is safe as long as `=&` is used. Was cause of my slightly misunderstanding of the assignment by reference! In the end was easier than I thought, and I was also able to do it by myself ^_^ Thanks – Kamafeather Jan 14 '15 at 09:38
  • Doing other tests, and I noticed here we need to assign by reference twice, either **on the function arguments declaration** `AND` **in the function assignment to `$temp`**. I wonder if someone else thinks this is ugly. --- I would expect that, being passed the argument by reference, the `$array` variable should be assigned by reference if I would do `$temp = $array` (cause the source argument is by-reference as well). Is that because the pass-by-ref in the function signature is just to prevent a copy of `$array`? Or there's some other reason? (_sorry for still bothering with noob questions_). – Kamafeather Feb 23 '15 at 20:45
  • @AbraCadaver, the other question you closed as duplicate is the exact complementer of this question. Here the nested array is given, there he asked how to create it from a different input format: http://stackoverflow.com/questions/34886008/create-nested-array-by-array-of-keys – Gavriel Jan 19 '16 at 21:05
  • 1
    Outstanding! Solved a three day problem. Thank you. – Alex Sarnowski Mar 02 '17 at 01:44
  • Is it also possible to `unset()` the references array? I mean, I could write `$temp = NULL;` but I'd rather `unset()`it. `unset(&$temp);` doesnt work though. – Philip Feb 15 '18 at 02:01
  • @Philip: Updated based on another question https://stackoverflow.com/questions/48931260/unset-element-in-multidimensional-array-by-path/48932206#48932206. Is this what you need? – AbraCadaver Feb 22 '18 at 18:11
  • Will not work if the path endpoint is an array (i.e `b.x` for your example) – user1032531 Dec 15 '18 at 14:14
  • @Abra please read this: https://stackoverflow.com/a/63914758/2943403 – mickmackusa Sep 21 '20 at 21:47
  • Example with objects support: https://gist.github.com/reduardo7/c3abf98f82362e91be2e803d8e2f8d1c – Eduardo Cuomo Nov 11 '20 at 17:41
5

I have solution for you not in the pure PHP, but using ouzo goodies concretely Arrays::getNestedValue method:

$arr = array('a' => 1,
    'b' => array(
        'y' => 2,
        'x' => array('z' => 5, 'w' => 'abc')
    ),
    'c' => null);

$key = 'b.x.z';
$path = explode('.', $key);

print_r(Arrays::getNestedValue($arr, $path));

Similarly if you need to set nested value you can use Arrays::setNestedValue method.

$arr = array('a' => 1,
    'b' => array(
        'y' => 2,
        'x' => array('z' => 5, 'w' => 'abc')
    ),
    'c' => null);

Arrays::setNestedValue($arr, array('d', 'e', 'f'), 'value');
print_r($arr);
Piotr Olaszewski
  • 6,017
  • 5
  • 38
  • 65
3

I have a utility I regularly use that I'll share. The difference being it uses array access notation (e.g. b[x][z]) instead of dot notation (e.g. b.x.z). With the documentation and code it is fairly self-explanatory.

<?php
class Utils {
    /**
     * Gets the value from input based on path.
     * Handles objects, arrays and scalars. Nesting can be mixed.
     * E.g.: $input->a->b->c = 'val' or $input['a']['b']['c'] = 'val' will
     * return "val" with path "a[b][c]".
     * @see Utils::arrayParsePath
     * @param mixed $input
     * @param string $path
     * @param mixed $default Optional default value to return on failure (null)
     * @return NULL|mixed NULL on failure, or the value on success (which may also be NULL)
     */
    public static function getValueByPath($input,$path,$default=null) {
        if ( !(isset($input) && (static::isIterable($input) || is_scalar($input))) ) {
            return $default; // null already or we can't deal with this, return early
        }
        $pathArray = static::arrayParsePath($path);
        $last = &$input;
        foreach ( $pathArray as $key ) {
            if ( is_object($last) && property_exists($last,$key) ) {
                $last = &$last->$key;
            } else if ( (is_scalar($last) || is_array($last)) && isset($last[$key]) ) {
                $last = &$last[$key];
            } else {
                return $default;
            }
        }
        return $last;
    }

    /**
     * Parses an array path like a[b][c] into a lookup array like array('a','b','c')
     * @param string $path
     * @return array
     */
    public static function arrayParsePath($path) {
        preg_match_all('/\\[([^[]*)]/',$path,$matches);
        if ( isset($matches[1]) ) {
            $matches = $matches[1];
        } else {
            $matches = array();
        }
        preg_match('/^([^[]+)/',$path,$name);
        if ( isset($name[1]) ) {
            array_unshift($matches,$name[1]);
        } else {
            $matches = array();
        }
        return $matches;
    }

    /**
     * Check if a value/object/something is iterable/traversable, 
     * e.g. can it be run through a foreach? 
     * Tests for a scalar array (is_array), an instance of Traversable, and 
     * and instance of stdClass
     * @param mixed $value
     * @return boolean
     */
    public static function isIterable($value) {
        return is_array($value) || $value instanceof Traversable || $value instanceof stdClass;
    }
}

$arr = array('a' => 1,
             'b' => array(
                 'y' => 2,
                 'x' => array('z' => 5, 'w' => 'abc')
             ),
             'c' => null);

$key = 'b[x][z]';

var_dump(Utils::getValueByPath($arr,$key)); // int 5

?>
zamnuts
  • 9,492
  • 3
  • 39
  • 46
  • Really useful! I already met something like this, following the same notation. I will compare the codes, thanks! Surely this notation will give more flexibility. I decide randomly for the dot, cause my use case doesn't need to be much complex. – Kamafeather Jan 13 '15 at 23:11
  • @Kamafeather, you could still use dot notation, just change the `arrayParsePath` method to `return explode('.',$path);` -- or add a test to see which path parser you should use and implement both! – zamnuts Jan 14 '15 at 01:06
  • Yes shouldn't be difficult. I guess working directly just on the regexp (and optionally pass it as argument to `getValueByPath()`) would be a better approach! Anyway nice advice, thanks! – Kamafeather Jan 14 '15 at 09:59
  • In PHP `else if` as two words is a violation of PSR-12 standards; it should be one word `elseif`. – mickmackusa Sep 21 '20 at 21:43
  • @mickmackusa PSR-12 was _proposed_ 7 months after this answer (Aug '15), and approved 4 years and 7 months (Aug '19); lmgtfy https://www.php-fig.org/psr/psr-12/meta/. Being a member with 30k reputation, you're welcome to suggest an edit that conforms to PSR-12. – zamnuts Oct 17 '20 at 22:49
  • There are a few other refinements that I could suggest (e.g. eliminating the `isset()` calls that follow the `preg_` calls by using the return value), but I don't have time to do a full review right now. If your answer does not represent the advice that you would currently give, please consider tuning it up. – mickmackusa Oct 18 '20 at 02:33
1

As a "getter", I've used this in the past:

$array = array('data' => array('one' => 'first', 'two' => 'second'));

$key = 'data.one';

function find($key, $array) {
    $parts = explode('.', $key);
    foreach ($parts as $part) {
        $array = $array[$part];
    }
    return $array;
}

$result = find($key, $array);
var_dump($result);
Mark Baker
  • 209,507
  • 32
  • 346
  • 385
1

If the keys of the array are unique, you can solve the problem in a few lines of code using array_walk_recursive:

    $arr = array('a' => 1,
        'b' => array(
            'y' => 2,
            'x' => array('z' => 5, 'w' => 'abc')
        ),
        'c' => null);

    function changeVal(&$v, $key, $mydata) {
        if($key == $mydata[0]) {
            $v = $mydata[1];
        }
    }

    $key = 'z';
    $value = '56';
    array_walk_recursive($arr, 'changeVal', array($key, $value));

    print_r($arr);
acontell
  • 6,792
  • 1
  • 19
  • 32
1

This is an approach using a static class. The benefits of this style is that your configuration will be globally accessible in your application.

It works by taking in a key path for example "database.mysql.username" and splitting the string into each of the key parts and moving a pointer to create a nested array.

The benefits of this approach is you can give a partial key and get back arrays of configuration values, you're not limited to just the end values. It also makes "default values" trivial to implement.

If you would like to have multiple configuration stores, just remove the static keywords and use it as an object instead.

Live Example

class Config
{
    private static $configStore = [];
    // This determines what separates the path
    // Examples: "." = 'example.path.value' or "/" = 'example/path/value'
    private static $separator = '.';

    public static function set($key, $value)
    {
        $keys = explode(self::$separator, $key);

        // Start at the root of the configuration array
        $pointer = &self::$configStore;

        foreach ($keys as $keySet) {
            // Check to see if a key exists, if it doesn't, set that key as an empty array
            if (!isset($pointer[$keySet])) {
                $pointer[$keySet] = [];
            }

            // Set the pointer to the current key
            $pointer = &$pointer[$keySet];
        }

        // Because we kept changing the pointer in the loop above, the pointer should be sitting at our desired location
        $pointer = $value;
    }

    public static function get($key, $defaultValue = null)
    {
        $keys = explode(self::$separator, $key);

        // Start at the root of the configuration array
        $pointer = &self::$configStore;

        foreach ($keys as $keySet) {
            // If we don't have a key as a part of the path, we should return the default value (null)
            if (!isset($pointer[$keySet])) {
                return $defaultValue;
            }
            $pointer = &$pointer[$keySet];
        }

        // Because we kept changing the pointer in the loop above, the pointer should be sitting at our desired location
        return $pointer;
    }
}

// Examples of how to use
Config::set('database.mysql.username', 'exampleUsername');
Config::set('database.mysql.password', 'examplePassword');
Config::set('database.mysql.database', 'exampleDatabase');
Config::set('database.mysql.host', 'exampleHost');

// Get back all the database configuration keys
var_dump(Config::get('database.mysql'));

// Get back a particular key from the database configuration
var_dump(Config::get('database.mysql.host'));

// Get back a particular key from the database configuration with a default if it doesn't exist
var_dump(Config::get('database.mysql.port', 3306));
Mattisdada
  • 979
  • 1
  • 15
  • 24
0

This function does the same as the accepted answer, plus is adds a third parameter by reference that is set to true/false if the key is present

function drupal_array_get_nested_value(array &$array, array $parents, &$key_exists = NULL) {
  $ref = &$array;
  foreach ($parents as $parent) {
    if (is_array($ref) && array_key_exists($parent, $ref)) {
      $ref = &$ref[$parent];
    }
    else {
      $key_exists = FALSE;
      $null = NULL;
      return $null;
    }
  }
  $key_exists = TRUE;
  return $ref;
}
AbraCadaver
  • 78,200
  • 7
  • 66
  • 87
0

I have a really simple and dirty solution to this (really dirty! DO NOT use if the value of the key is untrusted!). It might be more efficient than looping through the array.

function array_get($key, $array) {
    return eval('return $array["' . str_replace('.', '"]["', $key) . '"];');
}

function array_set($key, &$array, $value=null) {
    eval('$array["' . str_replace('.', '"]["', $key) . '"] = $value;');
}

Both of these functions do an eval on a snippet of code where the key is converted to an element of the array as PHP code. And it returns or sets the array value at the corresponding key.

Cave Johnson
  • 6,499
  • 5
  • 38
  • 57
0

Yet another solution for getter, using plain array_reduce method

@AbraCadaver's solution is nice, but not complete:

  • missing optional separator parameter & split if required
  • it raises an error in case of trying to obtain a key from a scalar value like 'one.two' from ['one' => 2]

My solution is:

function get ($array, $path, $separator = '.') {
    if (is_string($path)) {
        $path = explode($separator, $path);
    }

    return array_reduce(
        $path,
        function ($carry, $item) {
            return $carry[$item] ?? null;
        },
        $array
    );
}

it requires PHP 7 due to ?? operator, but this can be changed for older versions pretty easy ...

qdev
  • 1,371
  • 1
  • 15
  • 18
-1

Here a simple code to access and manipulate MD array. But there's no securities.

setter :

eval('$vars = &$array["' . implode('"]["', explode('.', strtolower($dot_seperator_path))) . '"];');
$vars = $new_value;

getter:

eval('$vars = $array["' . implode('"]["', explode('.', strtolower($dot_seperator_path))) . '"];');
return $vars;
Gino
  • 69
  • 3