使用字符串路径设置嵌套数组数据

51

我有一个不寻常的用例需要编写代码。 目标是这样的:我希望客户能够提供一个字符串,比如:

"cars.honda.civic = On"

使用这个字符串,我的代码将按以下方式设置一个值:

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

将客户输入进行标记化处理很容易:

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

现在,我该如何使用$ exploded路径来设置数组,而不必做诸如eval之类的恶劣事情?

8个回答

75

使用引用运算符来获取连续存在的数组:

$temp = &$data;
foreach($exploded as $key) {
    $temp = &$temp[$key];
}
$temp = $value;
unset($temp);

有没有一种方法可以获取值而不是设置它? - Marais Rossouw
@MaraisRossouw 你的评论/问题已经有一段时间了,但是可以看一下这个链接:https://dev59.com/Q2kw5IYBdhLWcg3wzd3Z#36042293 它可以作为getter和/或setter使用。 - Brad Kent
最后两行不是多余的吗?如果我们需要取消 $temp,那么为什么要在它上面一行设置它呢? - Mohd Abdul Mujib
$temp 是一个引用,倒数第二行写入了被引用的变量(在这种情况下是嵌套数组项),最后一行移除了引用,因此 $temp 不再与该变量相关联。 - alexisdm
啊...明白了。谢谢你的解释。 - Mohd Abdul Mujib

17

根据alexisdm的回答

/**
 * Sets a value in a nested array based on path
 * See https://dev59.com/Q2kw5IYBdhLWcg3wzd3Z#9628276
 *
 * @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
小调整:如果路径上的任何一个“节点”已经设置但不是数组,这个函数将会致命。 $a = ['foo'=>'not an array']; set_nested_array($a, 'foo/bar', 'new value'); 修复:在 foreach 的第一行插入以下代码 if (!is_array($current)) { $current = array(); } - Brad Kent

10

经过充分测试并且百分之百可用的代码。使用“parents”从数组中设置、获取和取消值。父元素可以是array('path', 'to', 'value')或字符串形式path.to.value。基于Drupal代码。

 /**
 * @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);
    }
}

1
谢谢您发布这个帖子!我刚开始为一个自定义的Drupal模块编写自己的版本,哈哈,我不知道这是核心功能。 - jeff-h

6

根据Ugo Méda的回复

这个版本

  • 允许你仅将其用作getter(不改变源数组)
  • 修复了遇到非数组值时的致命错误问题(Cannot create references to/from string offsets nor overloaded objects

无致命错误示例

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

$a现在是

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

作为getter使用

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

将值设置为null

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

$prevVal 是 "new value"
$a 现在是

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

你可以选择检查路径是否为字符串,并使用explode将其转换为数组,例如 $path = explode('.', $path); 这样你就可以使用流行的点符号表示法,例如 $val = arrayPath($a, 'foo.bar'); - AVProgrammer
PHP致命错误:只有变量可以通过引用传递在arrayPath($a, ['foo','bar'], 'new value'); - Peter Krauss

6
$data = $value;
foreach (array_reverse($exploded_path) as $key) {
    $data = array($key => $data);
}

1
除非您使用类似于array_merge_recursive的东西,否则您正在替换已经包含在$data中的先前值。 - alexisdm
1
这实际上是一个很好的观点,假设$data已经包含了值。 - deceze

5

你需要使用Symfony 属性路径

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

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

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

正是我所需要的! - StockBreak
这个解决方案的问题在于,如果属性不存在,它不会创建该属性。 - lotfi Raghib

-2

这正是这个方法的用途:

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

它接受您的$array,其中应设置元素,并以点分隔格式或连续键的数组接受$keys

因此,在您的情况下,您可以通过以下方式轻松实现所需的结果:

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

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

此外,$keys 参数还可以接受创建自动索引,因此您可以像这样使用它:
$data = Arr::set([], "cars.honda.civic.[]", 'On');

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


-7

你不能只是这样做吗?

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

3
我认为 $exp 的数量并不固定 - deceze
deceze 是正确的 - 我不能假设我会知道客户会提供多少节点。不过还是谢谢你的反馈。 - Anthony

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接