Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I alter array keys and values while using a RecursiveArrayIterator?

I suspect I'm doing something stupid here, but I'm confused by what seems like a simple problem with SPL:

How do I modified the contents of an array (the values in this example), using a RecursiveArrayIterator / RecursiveIteratorIterator?

Using the follow test code, I can alter the value within the loop using getInnerIterator() and offsetSet(), and dump out the modified array while I'm within the loop.

But when I leave the loop and dump the array from the iterator, it's back to the original values. What's happening?

$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);
like image 451
John Carter Avatar asked Aug 04 '09 16:08

John Carter


4 Answers

It seems that values in plain arrays aren't modifiable because they can't be passed by reference to the constructor of ArrayIterator (RecursiveArrayIterator inherits its offset*() methods from this class, see SPL Reference). So all calls to offsetSet() work on a copy of the array.

I guess they chose to avoid call-by-reference because it doesn't make much sense in an object-oriented environment (i. e. when passing instances of ArrayObject which should be the default case).

Some more code to illustrate this:

$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());
like image 117
mermshaus Avatar answered Oct 31 '22 22:10

mermshaus


You need to call getSubIterator at the current depth, use offsetSet at that depth, and do the same for all depths going back up the tree.

This is really useful for doing unlimited level array merge and replacements, on arrays or values within arrays. Unfortunately, array_walk_recursive will NOT work in this case as that function only visits leaf nodes.. so the 'replace_this_array' key in $array below will never be visited.

As an example, to replace all values within an array unknown levels deep, but only those that contain a certain key, you would do the following:

$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'
        ]
    ]
];
like image 31
John Joseph Avatar answered Oct 31 '22 21:10

John Joseph


Not using the Iterator classes (which seem to be copying data on the RecursiveArrayIterator::beginChildren() instead of passing by reference.)

You can use the following to achieve what you want

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

or use create_function() instead of creating the drop_200 function, but your mileage may vary with the create_function and memory usage.

like image 4
null Avatar answered Oct 31 '22 20:10

null


Looks like getInnerIterator creates a copy of the sub-iterator.

Maybe there is a different method? (stay tuned..)


Update: after hacking at it for a while, and pulling in 3 other engineers, it doesn't look like PHP gives you a way to alter the values of the subIterator.

You can always use the old stand by:

<?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;
        }
    }
}
?>
like image 2
Lance Rushing Avatar answered Oct 31 '22 21:10

Lance Rushing