11

I need to loop through an array twice - once to modify its values, and once to display its content in html. Unfortunately, I am running into trouble. I have created a test scenario to illustrate the issue.

$cases = array(
  array('caseStyle' => 'case style 1', 'caseNum' => 'case01'),
  array('caseStyle' => 'case style 2', 'caseNum' => 'case02'),
  array('caseStyle' => 'case style 3', 'caseNum' => 'case03'),
  array('caseStyle' => 'case style 4', 'caseNum' => 'case04'),
  array('caseStyle' => 'case style 5', 'caseNum' => 'case05'),
  array('caseStyle' => 'case style 6', 'caseNum' => 'case06')
);

foreach ($cases as $k => &$v) {
    $v['caseNum'] = ucwords($v['caseNum']);
}

foreach ($cases as $k => $v) {
    echo $v['caseNum'] . ' - ' . $v['caseStyle'] . '<br/>';
}

This outputs:

Case01 - case style 1
Case02 - case style 2
Case03 - case style 3
Case04 - case style 4
Case05 - case style 5
Case05 - case style 5

Note the values for the last item are wrong.

If during the second iteration I use foreach($cases as $k => $d) instead of foreach($cases as $k => $v) or if I unset $v before the second iteration (unset($v)) everything goes fine. This is intriguing. What am I missing?

DiMono
  • 3,308
  • 2
  • 19
  • 40
longwing
  • 111
  • 1
  • 1
  • 3
  • 3
    Why do you need to do this in two loops? Why not do both actions the first time through? – DiMono Sep 07 '13 at 03:52
  • In the actual code the array is one bulky bad ass array of arrays with rows returned from the db. In the first iteration I cleanup, add defaults for missing values, change formats where needed and add additional fields pulled from other tables to it. The second iteration is when I am painting the html page with values from the array (this is legacy code - html and php all in one file, no templates :( ) – longwing Sep 07 '13 at 03:59
  • 1
    That still doesn't mean you can't do it all at once. Even if you need to use values from the array before displaying them, you can buffer the output into a variable and then echo out the variable when needed. If complexity is causing a problem, remove the complexity ;) – DiMono Sep 07 '13 at 04:02
  • 1
    Your point is well taken. For that matter, simply using another variable in foreach or unsetting $v before the second iteration is solving the problem. However, that would be a blind solution. I need to understand what is happening here. – longwing Sep 07 '13 at 04:08
  • I've provided a thorough explanation of what's going on in an answer below. – DiMono Sep 07 '13 at 04:35
  • instead of using $v in second loop, use something else – dev.meghraj Sep 07 '13 at 04:38
  • Variable $v from first loop reassigned in second loop. – MD SHAHIDUL ISLAM Sep 07 '13 at 05:06

3 Answers3

22

When you execute a foreach loop, the variables in the () persist after the loop is done. That means that after the first loop finishes, you can var_dump($v) and get the values that were contained in it after the last iteration through the first loop. This is the case whether it's a reference (&$v) or a normal variable ($v).

However, if it's a reference in the first loop, it remains a reference unless it's unset. That means when you enter the second loop, you're overwriting the reference with the value of the array element you're currently looking at.

Remember, what foreach($cases as $k => $v) really means is "take the key for this element in $cases and assign that to $k, and take the value for this element and assign it to $v)". Since $v is still a reference to the last element in the array, rather than setting the value of a new variable $v, you're actually updating the value of where $v already points to.

What that means is, if we simplify $cases to be simply ['a', 'b', 'c', 'd'], after the first time through the second foreach, $cases is now ['a', 'b', 'c', 'a'] because you've reassigned the element in $cases that $v points to - the last one - to have the same value as the first one. The second time through, it's ['a', 'b', 'c', 'b']. The third time through it's ['a', 'b', 'c', 'c']. Then, the last time through, you're assigning it to itself, and at that time it holds the value 'c'.

This is really just a case of php working as expected. The solution is to unset($v) as soon as the first loop finishes, to make sure that the next time you use $v you're using a new variable, rather than an existing reference.

To see this in action:

Head on over to http://phpfiddle.org/ and paste the following code, and run it; you'll see in the output that $v is maintained after the first loop completes, and that the value of $cases[5] changes each time through the second loop.

$cases = array(
  array('caseStyle' => 'case style 1', 'caseNum' => 'case01'),
  array('caseStyle' => 'case style 2', 'caseNum' => 'case02'),
  array('caseStyle' => 'case style 3', 'caseNum' => 'case03'),
  array('caseStyle' => 'case style 4', 'caseNum' => 'case04'),
  array('caseStyle' => 'case style 5', 'caseNum' => 'case05'),
  array('caseStyle' => 'case style 6', 'caseNum' => 'case06')
);

foreach ($cases as $k => &$v) {
    $v['caseNum'] = ucwords($v['caseNum']);
}
var_dump($v);
echo "<br />";
foreach ($cases as $k => $v) {
    print_r($cases);
    echo "<br />";
    echo $k . ': ' . $v['caseNum'] . ' - ' . $v['caseStyle'] . '<br/>';
}
DiMono
  • 3,308
  • 2
  • 19
  • 40
2

Try this:

foreach ($cases as $k => &$v) {
    $cases[$k]['caseNum'] = ucwords($v['caseNum']);
    echo $cases[$k]['caseNum'] . ' - ' . $cases[$k]['caseStyle'] . '<br/>';
}
undone
  • 7,857
  • 4
  • 44
  • 69
  • Hm also read that question and was trying to reproduce that on XAMPP. While the initial code wouldn't be my first attempt, I tried to figure out what's going wrong. Didn't come to a conclusion, but your solution definitely works. – grobmotoriker Sep 07 '13 at 04:16
  • This is what we've been discussing in the question's comments. – DiMono Sep 07 '13 at 04:17
  • @undone - Thanks for your answer. Unfortunately, this is not usable in my original script. I need to do it twice. Unsetting $v between the 2 loops seems the best solution. – longwing Sep 07 '13 at 11:25
0

Let me know is this your solution:

foreach ($cases as $k => &$v){
    foreach($v as &$value){ $value = ucwords($value); }
echo $v['caseNum'] . ' - ' . $v['caseStyle'] . '<br/>';
}
01e
  • 691
  • 3
  • 14
  • Thanks for sharing. Once you pass $v by reference, all its children are automatically passed by reference. No need to explicitly do so. – longwing Sep 07 '13 at 11:27
  • In second foreach the $v variable treat as $cases! what you do is reference the values of $v into $value not copy of them. – 01e Sep 07 '13 at 11:59