54

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?

0

8 Answers 8

79

Use the reference operator to get the successive existing arrays:

$temp = &$data;
foreach($exploded as $key) {
    $temp = &$temp[$key];
}
$temp = $value;
unset($temp);
Sign up to request clarification or add additional context in comments.

4 Comments

Is there a way to get a value instead of setting?
@MaraisRossouw your comment/question is old, but take a look at stackoverflow.com/a/36042293/1371433 it works a s a getter and/or setter
Aren't the last two lines redundant? Like If we need to unset $temp then why even set it just a line above?
@MohdAbdulMujib $temp is a reference, 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.
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;
}

1 Comment

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

2 Comments

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.
FYI This was present in D7 but I see no equivalent in D8+ api.drupal.org/api/drupal/includes%21common.inc/function/…
6
$data = $value;
foreach (array_reverse($exploded_path) as $key) {
    $data = array($key => $data);
}

2 Comments

Unless you use something like array_merge_recursive, you are replacing the previous values already that $data already contains.
That's actually a good point, assuming that $data already does contain values.
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;
}

2 Comments

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');
"PHP Fatal error: Only variables can be passed by reference" on arrayPath($a, ['foo','bar'], 'new value');
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 Comments

Exactly what I needed!
the problem with this solution, it won't create the attribut if not exist
-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'],
    ],
  ],
];

Comments

-7

Can't you just do this

$exp = explode(".",$path);
$array[$exp[0]][$exp[1]][$exp[2]] = $value

2 Comments

I'd simply assume that the number of $exp is not fixed!
deceze is correct - I can't assume I'll know how many nodes the customer will provide. Thanks for the feedback, though.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.