views:

371

answers:

5

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

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;
        }
    }
}
?>
Lance Rushing
I don't think it's as simple as getInnerIterator creating a copy, since `$cRecursiveIter->getArrayCopy()` within the loop gives the modified value
therefromhere
A: 

I know this doesn't answer your question directly, but it's not a good practice to modify the object under iteration while iterating over it.

Adam Byrtek
Are you sure about that? The `RecursiveArrayIterator` docs say "This iterator allows to unset and modify values and keys while iterating over Arrays and Objects...".
therefromhere
A: 

Could it come down to passing by reference vs passing by value?

For example try changing:

$cArray = new ArrayObject($aNestedArray);

to:

$cArray = new ArrayObject(&$aNestedArray);
Swish
therefromhere
+2  A: 

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());
Marc Ermshaus
A: 

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.

null