Как изменить ключи и значения массива при использовании RecursiveArrayIterator? - PullRequest
16 голосов
/ 04 августа 2009

Я подозреваю, что я делаю здесь что-то глупое, но меня смущает то, что кажется простой проблемой с SPL:

Как изменить содержимое массива (значения в этом примере), используя RecursiveArrayIterator / RecursiveIteratorIterator ?

Используя следующий тестовый код, я могу изменить значение в цикле с помощью getInnerIterator () и offsetSet () и вывести измененный массив, пока я нахожусь в пределах цикл.

Но когда я выхожу из цикла и выгружаю массив из итератора, он возвращается к исходным значениям. Что происходит?

$aNestedArray = array();
$aNestedArray[101] = range(100, 1000, 100);
$aNestedArray[201] = range(300, 25, -25);
$aNestedArray[301] = range(500, 0, -50);

$cArray = new ArrayObject($aNestedArray);
$cRecursiveIter = new RecursiveIteratorIterator(new RecursiveArrayIterator($cArray), RecursiveIteratorIterator::LEAVES_ONLY);

// Zero any array elements under 200  
while ($cRecursiveIter->valid())
{
    if ($cRecursiveIter->current() < 200)
    {
        $cInnerIter = $cRecursiveIter->getInnerIterator();
        // $cInnerIter is a RecursiveArrayIterator
        $cInnerIter->offsetSet($cInnerIter->key(), 0);
    }

    // This returns the modified array as expected, with elements progressively being zeroed
    print_r($cRecursiveIter->getArrayCopy());

    $cRecursiveIter->next();
}

$aNestedArray = $cRecursiveIter->getArrayCopy();

// But this returns the original array.  Eh??
print_r($aNestedArray);

Ответы [ 7 ]

5 голосов
/ 11 августа 2009

Кажется, что значения в простых массивах нельзя изменить, потому что они не могут быть переданы по ссылке на конструктор ArrayIterator (RecursiveArrayIterator наследует свои offset*() методы от этого класса, см. SPL Reference ). Таким образом, все вызовы offsetSet() работают с копией массива.

Я полагаю, что они решили избежать вызова по ссылке, потому что это не имеет особого смысла в объектно-ориентированной среде (т. Е. При передаче экземпляров ArrayObject, что должно быть случаем по умолчанию).

Еще немного кода, чтобы проиллюстрировать это:

$a = array();

// Values inside of ArrayObject instances will be changed correctly, values
// inside of plain arrays won't
$a[] = array(new ArrayObject(range(100, 200, 100)),
             new ArrayObject(range(200, 100, -100)),
             range(100, 200, 100));
$a[] = new ArrayObject(range(225, 75, -75));

// The array has to be
//     - converted to an ArrayObject or
//     - returned via $it->getArrayCopy()
// in order for this field to get handled properly
$a[] = 199;

// These values won't be modified in any case
$a[] = range(100, 200, 50);

// Comment this line for testing
$a = new ArrayObject($a);

$it = new RecursiveIteratorIterator(new RecursiveArrayIterator($a));

foreach ($it as $k => $v) {
    // getDepth() returns the current iterator nesting level
    echo $it->getDepth() . ': ' . $it->current();

    if ($v < 200) {
        echo "\ttrue";

        // This line is equal to:
        //     $it->getSubIterator($it->getDepth())->offsetSet($k, 0);
        $it->getInnerIterator()->offsetSet($k, 0);
    }

    echo ($it->current() == 0) ? "\tchanged" : '';
    echo "\n";
}

// In this context, there's no real point in using getArrayCopy() as it only
// copies the topmost nesting level. It should be more obvious to work with $a
// itself
print_r($a);
//print_r($it->getArrayCopy());
3 голосов
/ 13 января 2010

Не использовать классы Iterator (которые, похоже, копируют данные на RecursiveArrayIterator::beginChildren() вместо передачи по ссылке.)

Вы можете использовать следующее для достижения желаемого

function drop_200(&$v) { if($v < 200) { $v = 0; } }

$aNestedArray = array();
$aNestedArray[101] = range(100, 1000, 100);
$aNestedArray[201] = range(300, 25, -25);
$aNestedArray[301] = range(500, 0, -50);

array_walk_recursive ($aNestedArray, 'drop_200');

print_r($aNestedArray);

или используйте create_function() вместо создания функции drop_200, но ваш пробег может варьироваться в зависимости от create_function и использования памяти.

2 голосов
/ 03 апреля 2017

Сначала преобразуйте массив в объект, и он будет работать как положено ..

    $array = [
        'one' => 'One',
        'two' => 'Two',
        'three' => [
            'four' => 'Four',
            'five' => [
                'six' => 'Six',
                'seven' => 'Seven'
            ]
        ]
    ];

    // Convert to object (using whatever method you want)
    $array = json_decode(json_encode($array));

    $iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($array));
    foreach($iterator as $key => $value) {
        $iterator->getInnerIterator()->offsetSet($key, strtoupper($value));
    }

    var_dump($iterator->getArrayCopy());
2 голосов
/ 04 августа 2009

Похоже, что getInnerIterator создает копию суб-итератора.

Может быть, есть другой метод? (следите за обновлениями ..)


Обновление: после некоторого взлома и привлечения трех других инженеров, похоже, что PHP не дает вам способа изменить значения subIterator.

Вы всегда можете использовать старый стенд:

<?php  
// Easy to read, if you don't mind references (and runs 3x slower in my tests) 
foreach($aNestedArray as &$subArray) {
    foreach($subArray as &$val) {
       if ($val < 200) {
            $val = 0;
        }
    }
}
?>

OR

<?php 
// Harder to read, but avoids references and is faster.
$outherKeys = array_keys($aNestedArray);
foreach($outherKeys as $outerKey) {
    $innerKeys = array_keys($aNestedArray[$outerKey]);
    foreach($innerKeys as $innerKey) {
        if ($aNestedArray[$outerKey][$innerKey] < 200) {
            $aNestedArray[$outerKey][$innerKey] = 0;
        }
    }
}
?>
1 голос
/ 08 ноября 2016

Вам необходимо вызвать getSubIterator на текущей глубине, использовать offsetSet на этой глубине и сделать то же самое для всех глубин, возвращающихся вверх по дереву.

Это действительно полезно для выполнения слияния и замены массивов неограниченного уровня для массивов или значений в массивах. К сожалению, array_walk_recursive НЕ будет работать в этом случае, так как эта функция посещает только конечные узлы .. поэтому ключ replace_this_array в массиве $ ниже никогда не будет посещен.

В качестве примера, чтобы заменить все значения в массиве на неизвестные уровни глубиной, но только на те, которые содержат определенный ключ, вы должны сделать следующее:

$array = [
    'test' => 'value',
    'level_one' => [
        'level_two' => [
            'level_three' => [
                'replace_this_array' => [
                    'special_key' => 'replacement_value',
                    'key_one' => 'testing',
                    'key_two' => 'value',
                    'four' => 'another value'
                ]
            ],
            'ordinary_key' => 'value'
        ]
    ]
];

$arrayIterator = new \RecursiveArrayIterator($array);
$completeIterator = new \RecursiveIteratorIterator($arrayIterator, \RecursiveIteratorIterator::SELF_FIRST);

foreach ($completeIterator as $key => $value) {
    if (is_array($value) && array_key_exists('special_key', $value)) {
        // Here we replace ALL keys with the same value from 'special_key'
        $replaced = array_fill(0, count($value), $value['special_key']);
        $value = array_combine(array_keys($value), $replaced);
        // Add a new key?
        $value['new_key'] = 'new value';

        // Get the current depth and traverse back up the tree, saving the modifications
        $currentDepth = $completeIterator->getDepth();
        for ($subDepth = $currentDepth; $subDepth >= 0; $subDepth--) {
            // Get the current level iterator
            $subIterator = $completeIterator->getSubIterator($subDepth); 
            // If we are on the level we want to change, use the replacements ($value) other wise set the key to the parent iterators value
            $subIterator->offsetSet($subIterator->key(), ($subDepth === $currentDepth ? $value : $completeIterator->getSubIterator(($subDepth+1))->getArrayCopy()));
        }
    }
}
return $completeIterator->getArrayCopy();
// return:
$array = [
    'test' => 'value',
    'level_one' => [
        'level_two' => [
            'level_three' => [
                'replace_this_array' => [
                    'special_key' => 'replacement_value',
                    'key_one' => 'replacement_value',
                    'key_two' => 'replacement_value',
                    'four' => 'replacement_value',
                    'new_key' => 'new value'
                ]
            ],
            'ordinary_key' => 'value'
        ]
    ]
];
0 голосов
/ 05 августа 2009

Может ли это быть результатом передачи по ссылке против передачи по значению?

Например, попробуйте изменить:

$cArray = new ArrayObject($aNestedArray);

до:

$cArray = new ArrayObject(&$aNestedArray);
0 голосов
/ 04 августа 2009

Я знаю, что это не отвечает на ваш вопрос напрямую, но не рекомендуется изменять итеративный объект при его итерации.

...