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);
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());
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'
]
]
];
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.
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;
}
}
}
?>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With