51

I have an unusual use-case I'm trying to code for. The goal is this: I want the customer to be able to provide a string, such as:

"cars.honda.civic = On"

Using this string, my code will set a value as follows:

$data['cars']['honda']['civic'] = 'On';

It's easy enough to tokenize the customer input as such:

$token = explode("=",$input);
$value = trim($token[1]);
$path = trim($token[0]);
$exploded_path = explode(".",$path);

But now, how do I use $exploded path to set the array without doing something nasty like an eval?

Anthony
  • 5,275
  • 11
  • 50
  • 86

8 Answers8

75

Use the reference operator to get the successive existing arrays:

$temp = &$data;
foreach($exploded as $key) {
    $temp = &$temp[$key];
}
$temp = $value;
unset($temp);
alexisdm
  • 29,448
  • 6
  • 64
  • 99
  • Is there a way to get a value instead of setting? – Marais Rossouw Oct 18 '14 at 06:59
  • @MaraisRossouw your comment/question is old, but take a look at http://stackoverflow.com/a/36042293/1371433 it works a s a getter and/or setter – Brad Kent Mar 16 '16 at 17:05
  • Aren't the last two lines redundant? Like If we need to unset `$temp` then why even set it just a line above? – Mohd Abdul Mujib Aug 12 '19 at 23:47
  • @MohdAbdulMujib `$temp` is a [reference](https://www.php.net/manual/en/language.references.whatare.php), the line before the last writes to the referenced variable (which is the nested array item in this case) and the last one removes the referencing, so that `$temp` isn't linked to that variable. – alexisdm Aug 13 '19 at 22:29
  • Ahh... Got it. Thanks for explaining. – Mohd Abdul Mujib Aug 13 '19 at 22:40
17

Based on alexisdm's response :

/**
 * Sets a value in a nested array based on path
 * See https://stackoverflow.com/a/9628276/419887
 *
 * @param array $array The array to modify
 * @param string $path The path in the array
 * @param mixed $value The value to set
 * @param string $delimiter The separator for the path
 * @return The previous value
 */
function set_nested_array_value(&$array, $path, &$value, $delimiter = '/') {
    $pathParts = explode($delimiter, $path);

    $current = &$array;
    foreach($pathParts as $key) {
        $current = &$current[$key];
    }

    $backup = $current;
    $current = $value;

    return $backup;
}
Community
  • 1
  • 1
Ugo Méda
  • 1,205
  • 8
  • 23
  • 1
    minor adjustment: this function will fatal if one of the "nodes" along the path is already set but not an array. `$a = ['foo'=>'not an array']; set_nested_array($a, 'foo/bar', 'new value');` fix: insert first as first line for foreach `if (!is_array($current)) { $current = array(); }` – Brad Kent Mar 16 '16 at 15:43
10

Well tested and 100% working code. Set, get, unset values from an array using "parents". The parents can be either array('path', 'to', 'value') or a string path.to.value. Based on Drupal's code

 /**
 * @param array $array
 * @param array|string $parents
 * @param string $glue
 * @return mixed
 */
function array_get_value(array &$array, $parents, $glue = '.')
{
    if (!is_array($parents)) {
        $parents = explode($glue, $parents);
    }

    $ref = &$array;

    foreach ((array) $parents as $parent) {
        if (is_array($ref) && array_key_exists($parent, $ref)) {
            $ref = &$ref[$parent];
        } else {
            return null;
        }
    }
    return $ref;
}

/**
 * @param array $array
 * @param array|string $parents
 * @param mixed $value
 * @param string $glue
 */
function array_set_value(array &$array, $parents, $value, $glue = '.')
{
    if (!is_array($parents)) {
        $parents = explode($glue, (string) $parents);
    }

    $ref = &$array;

    foreach ($parents as $parent) {
        if (isset($ref) && !is_array($ref)) {
            $ref = array();
        }

        $ref = &$ref[$parent];
    }

    $ref = $value;
}

/**
 * @param array $array
 * @param array|string $parents
 * @param string $glue
 */
function array_unset_value(&$array, $parents, $glue = '.')
{
    if (!is_array($parents)) {
        $parents = explode($glue, $parents);
    }

    $key = array_shift($parents);

    if (empty($parents)) {
        unset($array[$key]);
    } else {
        array_unset_value($array[$key], $parents);
    }
}
ymakux
  • 3,415
  • 1
  • 34
  • 43
  • 1
    Thanks for posting this! I was just beginning to write my own version, for a custom Drupal module lol I had no idea this was in core. – jeff-h Sep 11 '18 at 03:00
6

Based on Ugo Méda's response :

This version

  • allows you to use it solely as a getter (leave the source array untouched)
  • fixes the fatal error issue if a non-array value is encountered (Cannot create references to/from string offsets nor overloaded objects)

no fatal error example

$a = ['foo'=>'not an array'];
arrayPath($a, ['foo','bar'], 'new value');

$a is now

array(
    'foo' => array(
        'bar' => 'new value',
    ),
)

Use as a getter

$val = arrayPath($a, ['foo','bar']);  // returns 'new value' / $a remains the same

Set value to null

$v = null; // assign null to variable in order to pass by reference
$prevVal = arrayPath($a, ['foo','bar'], $v);

$prevVal is "new value"
$a is now

array(
    'foo' => array(
        'bar' => null,
    ),
)

 

/**
 * set/return a nested array value
 *
 * @param array $array the array to modify
 * @param array $path  the path to the value
 * @param mixed $value (optional) value to set
 *
 * @return mixed previous value
 */
function arrayPath(&$array, $path = array(), &$value = null)
{
    $args = func_get_args();
    $ref = &$array;
    foreach ($path as $key) {
        if (!is_array($ref)) {
            $ref = array();
        }
        $ref = &$ref[$key];
    }
    $prev = $ref;
    if (array_key_exists(2, $args)) {
        // value param was passed -> we're setting
        $ref = $value;  // set the value
    }
    return $prev;
}
Community
  • 1
  • 1
Brad Kent
  • 4,982
  • 3
  • 22
  • 26
  • You can optionally check if path is a string, and convert to an array with explode, eg. `$path = explode('.', $path);` so you can use the popular dot notation, eg. `$val = arrayPath($a, 'foo.bar');` – AVProgrammer May 11 '16 at 18:22
  • "PHP Fatal error: Only variables can be passed by reference" on `arrayPath($a, ['foo','bar'], 'new value');` – Peter Krauss Jun 14 '18 at 21:11
6
$data = $value;
foreach (array_reverse($exploded_path) as $key) {
    $data = array($key => $data);
}
deceze
  • 510,633
  • 85
  • 743
  • 889
  • 1
    Unless you use something like `array_merge_recursive`, you are replacing the previous values already that $data already contains. – alexisdm Mar 09 '12 at 02:58
  • 1
    That's actually a good point, assuming that `$data` already does contain values. – deceze Mar 09 '12 at 03:04
5

You need use Symfony PropertyPath

<?php
// ...
$person = array();

$accessor->setValue($person, '[first_name]', 'Wouter');

var_dump($accessor->getValue($person, '[first_name]')); // 'Wouter'
// or
// var_dump($person['first_name']); // 'Wouter'
-2

This is exactly what this method is for:

Arr::set($array, $keys, $value);

It takes your $array where the element should be set, and accept $keys in dot separated format or array of subsequent keys.

So in your case you can achieve desired result simply by:

$data = Arr::set([], "cars.honda.civic", 'On');

// Which will be equivalent to
$data = [
  'cars' => [
    'honda' => [
      'civic' => 'On',
    ],
  ],
];

What's more, $keys parameter can also accept creating auto index, so you can for example use it like this:

$data = Arr::set([], "cars.honda.civic.[]", 'On');

// In order to get
$data = [
  'cars' => [
    'honda' => [
      'civic' => ['On'],
    ],
  ],
];

Minwork
  • 838
  • 8
  • 9
-7

Can't you just do this

$exp = explode(".",$path);
$array[$exp[0]][$exp[1]][$exp[2]] = $value
Starx
  • 77,474
  • 47
  • 185
  • 261