如何通过键名/路径访问和操作多维数组?

25

我需要在PHP中实现一个setter,它允许我指定一个数组的键或子键(目标),并将名称作为点分隔的键值传递。

给定以下代码:

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

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

我希望从$key的值中获取$arr['b']['x']['z']的值5

现在,给定变量值$key和一个不同的$arr值(具有不同的深度)。

如何设置由$key指向的元素的值?

对于getter get(),我编写了以下代码:

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;
}
编写setter函数更加困难,因为我能够成功地找到正确的元素(从$key中),但我无法在原始数组中设置该值,也不知道如何一次指定所有键。我应该使用某种回溯吗?还是可以避免它?

1
这个回答解决了你的问题吗?使用字符串访问(可能很大的)多维数组 - Brombomb
10个回答

34
假设$path已经通过explode(或添加到函数中)成为一个数组,那么您可以使用引用。 您需要添加一些错误检查以防$path无效等情况(考虑isset):
$key = 'b.x.z';
$path = explode('.', $key);

获取器

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

这个组合将在现有的数组中设置一个值,或者如果你传递一个尚未定义的数组,则创建该数组。请确保定义$array作为引用传递&$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

这将会取消设定路径中的最终键值:

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

*原始答案有一些有限的功能,我将保留以防它们对某人有用:

Setter

请确保定义要传递的$array是引用&$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');

或者如果您想返回更新后的数组(因为我感到无聊):

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

创建者

如果你想创建一个数组并可选地设置值:

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

有趣的事情

根据字符串b.x.z,构建并评估类似于$array['b']['x']['z']的内容:

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

    return $result;
}

设置类似于$array ['b'] ['x'] ['z'] ='一些值';的东西:
function set(&$array, $path, $value) {
    //$path = explode('.', $path); //if needed
    $path = "['" . implode("']['", $path) . "']";
    eval("\$array{$path} = $value;");
}

取消类似于$array['b']['x']['z']的设置:

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

你知道吗?我自己写了同样的代码,但是我没有测试就把它丢掉了,因为我认为每次将值重新分配给同一个变量会改变中间的数组值(在达到所需值之前),从而引发不可预测的副作用!相反,只要使用=&就是安全的。这是我对按引用赋值的略微误解的原因!最后比我想象的要容易,我也能够自己做到 ^_^ 谢谢 - Kamafeather
@AbraCadaver,你关闭的另一个重复问题恰好是这个问题的确切补充。 这里给出了嵌套数组,而他问如何从不同的输入格式创建它:https://dev59.com/VpPea4cB1Zd3GeqP_zDb - Gavriel
1
太棒了!解决了一个三天的问题。谢谢。 - Alex Sarnowski
@Philip:根据另一个问题https://stackoverflow.com/questions/48931260/unset-element-in-multidimensional-array-by-path/48932206#48932206进行了更新。这符合您的需求吗? - AbraCadaver
@Abra请阅读此内容:https://dev59.com/2nI-5IYBdhLWcg3wm5rN#63914758 - mickmackusa
显示剩余4条评论

5

我有一个解决方案,不是使用纯PHP,而是利用ouzo goodies,具体地说是Arrays::getNestedValue方法:

$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));

同样地,如果您需要设置嵌套值,可以使用Arrays::setNestedValue方法。

$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);

4

我有一个实用工具经常使用,我可以分享给你。不同之处在于它使用数组访问符号(例如 b[x][z])而不是点符号(例如 b.x.z)。通过文档和代码,这个工具相当容易理解。

<?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

?>

非常有用! 我之前遇到过类似的东西,使用相同的符号表示法。我会比较一下代码,谢谢! 这种符号表示法肯定会提供更多的灵活性。 我随机选择了点,因为我的用例不需要太复杂。 - Kamafeather
@Kamafeather,你仍然可以使用点表示法,只需将arrayParsePath方法更改为return explode('.',$path);--或添加一个测试以查看应该使用哪个路径解析器并实现两者! - zamnuts
不应该很难。我猜直接在正则表达式上工作(并可选择将其作为参数传递给 getValueByPath())会是更好的方法!无论如何,好建议,谢谢! - Kamafeather
在 PHP 中,else if 作为两个单词违反了 PSR-12 标准;应该写成一个单词 elseif - mickmackusa
@mickmackusa 在这个回答发布7个月后('15年8月)提出了PSR-12,并在4年7个月后('19年8月)获得批准; 请参考 https://www.php-fig.org/psr/psr-12/meta/。作为一个拥有30k声望的成员,您可以建议编辑以符合PSR-12。 - zamnuts
显示剩余2条评论

1
作为“getter”,我过去曾经使用过这个代码:

$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);

1
如果数组的键是唯一的,您可以使用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);

不幸的是,我的情况不能保证键是唯一的。 但我会继续关注这个问题。谢谢 - Kamafeather

1

这是一种使用静态类的方法。这种风格的好处是您的配置将在应用程序中全局访问。

它通过获取键路径,例如“database.mysql.username”,并将字符串拆分为每个键部分并移动指针以创建嵌套数组来工作。

这种方法的好处是您可以提供部分键并返回配置值数组,您不仅限于最终值。它还使“默认值”易于实现。

如果您想要多个配置存储,请删除静态关键字并将其用作对象。

示例

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

0

这个函数与被接受的答案相同,但它添加了一个第三个参数,通过引用设置为true/false,以指示键是否存在。

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

0

使用普通的array_reduce方法,另一种getter的解决方案。

@AbraCadaver的解决方案很好,但不完整:

  • 缺少可选的分隔符参数和必要的拆分
  • 如果尝试从标量值(如'one.two')中获取键,则会引发错误,例如从['one' => 2]中获取

我的解决方案是:

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

由于使用了??运算符,所以需要PHP 7,但是这很容易在旧版本中更改...


0

我有一个非常简单而且不太优雅的解决方案(真的很不优雅!如果键的值不可信,请勿使用!)。它可能比循环数组更有效率。

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

这两个函数都对一段代码片段执行 eval,其中键被转换为 PHP 代码数组的一个元素。它返回或设置相应键的数组值。


-1
这是一个用于访问和操作MD数组的简单代码,但没有安全性。
setter:
eval('$vars = &$array["' . implode('"]["', explode('.', strtolower($dot_seperator_path))) . '"];');
$vars = $new_value;

获取器:

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

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