12

I have a couple of queries about modifying an array during a foreach() loop. In the code below I loop through three arrays that contain closures/callbacks and invoke each one. I append a closure to the end of each array during iteration, however sometimes foreach() doesn't seem to recognise that the array has changed size and so the appended closure doesn't get called.

class Foo
{
    private $a1 = array();
    private $a2 = array();

    public function f()
    {
        echo '<pre style="font-size: 20px;">';
        echo 'PHP: ' . phpversion() . '<br><br>';

        $this->a1[] = function() { echo 'a1 '; };
        $this->a1[] = array($this, 'g');
        foreach ($this->a1 as &$v)
        {
            // The callback added in g() never gets called.
            call_user_func($v);
            //echo 'count(v) = ' . count($v) . ' ';
        }

        echo '<br>';

        // The same thing works fine with a for() loop.
        $this->a2[] = function() { echo 'a2 '; };
        $this->a2[] = array($this, 'h');
        for ($i = 0; $i < count($this->a2); ++$i)
            call_user_func($this->a2[$i]);

        echo '<br>';

        // It also works fine using a local array as long as it
        // starts off with more than one element.
        $a3[] = function() { echo 'a3 '; };
        //$a3[] = function() { echo 'a3 '; };
        $i = 0;
        foreach ($a3 as &$x)
        {
            call_user_func($x);
            if ($i++ > 1) // prevent infinite loop
                break;

            // Why does this get called only if $a3 originally starts
            // with more than one element?
            $a3[] = function() { echo 'callback '; };
        }

        echo '</pre>';
    }

    private function g()
    {
        echo 'g() ';
        $this->a1[] = function() { echo 'callback '; };
    }

    private function h()
    {
        echo 'h() ';
        $this->a2[] = function() { echo 'callback '; };
    }
}

$foo = new Foo;
$foo->f();

Output:

PHP: 5.3.14-1~dotdeb.0

a1 g() 
a2 h() callback 
a3

Expected output:

a1 g() callback
a2 h() callback 
a3 callback

Output for $a3 if I uncomment the second element before the loop:

a3 a3 callback
  1. Why doesn't the first loop foreach ($this->a1 as &$v) realise $v has another element to iterate over?
  2. Why does modifying $a3 work during the third loop foreach ($a3 as &$x), but only when the array starts off with more than one element?

I realise modifying an array during iteration is probably not a good idea, but since PHP seems to allow it I'm curious why the above works the way it does.

4
  • Well firstly, you are modifying the array while it is being looped over and PHP really wasnt designed with that in mind. you're probably better off doing a callback function in the loop and using foreach without referencing. Commented Jan 31, 2013 at 19:22
  • @Hiroto Well the manual makes mention of it so I think PHP is designed with this in mind. However I realise it's probably not good practice, but I'm still wanting to know why it behaves as it does, it seems inconsistent. Commented Jan 31, 2013 at 19:32
  • 1
    I made a quick class that essentially does what your class does and came up with the same result. According to the php foreach page, while (list(, $value) = each($arr)) is supposed to be exactly the same as a foreach, however I had no* problems when using the while loop instead of the foreach. Commented Jan 31, 2013 at 19:33
  • It mentions it because it can be done, but PHP's foreach was designed to copy the array and iterate over it, because you don't run into infinite loops that way. as Rasmus Lerdorf puts it: "Ugly problems often require ugly solutions. Solving an ugly problem in a pure manner is bloody hard.". The "elegant" solution would likely be using a callback function rather than relying on the array. Commented Jan 31, 2013 at 19:41

2 Answers 2

6

Interesting observation:

echo "foreach:  ";
$a = array(1,2,3);
foreach($a as $v) {
  echo $v, " ";
  if ($v===1) $a[] = 4;
  if ($v===4) $a[] = 5;
}

echo "\nforeach&: ";
$a = array(1,2,3);
foreach($a as &$v) {
  echo $v, " ";
  if ($v===1) $a[] = 4;
  if ($v===4) $a[] = 5;
}

echo "\nwhile:    ";
$a = array(1,2,3);
while(list(,$v) = each($a)) {
  echo $v, " ";
  if ($v===1) $a[] = 4;
  if ($v===4) $a[] = 5;
}

echo "\nfor:      ";
$a = array(1,2,3);
for($v=reset($a); key($a)!==null; $v=next($a)) {
  echo $v, " ";
  if ($v===1) $a[] = 4;
  if ($v===4) $a[] = 5;
}

results in

foreach:  1 2 3 
foreach&: 1 2 3 4 
while:    1 2 3 4 5 
for:      1 2 3 4 5 

This means:

  • a normal foreach loop operates on a copy of the array, any modifications of the array within the loop do not affect the loop
  • a foreach with referenced value is forced to use the original array but advances the array pointer before each iteration after assigning key and value variables. Also there is some optimization going on that prevents another check as soon as the pointer reaches the end. So at the beginning of the last iteration the loop is told to run once more and then finish - no more interfering possible.
  • a while loop with each() advances the array pointer just like foreach does but explicitly checks it again after the last iteration
  • a for loop where the array pointer is advanced after each iteration obviously has no problems with changing the array at any point.
Sign up to request clarification or add additional context in comments.

2 Comments

Since PHP 8.0 removed the each() operator (deprecated since 7.2), is there a way to replicate the behaviour with the explicit check?
Your second example foreach&: is not longer valid. at least for PHP 8 it prints 1 2 3 4 5
4

1.Why doesn't the first loop foreach ($this->a1 as &$v) realise $v has another element to iterate over?

The behaviour looks to be due to the internal pointer being advanced on the array on each foreach iteration. Adding an array element to the end of the array on the last iteration of the array, that is when the internal pointer is already null, means that this element will not be iterated over. With some modifications to your code, this can be seen.

class Foo
{
    private $a1 = array();
    private $a2 = array();

    public function f()
    {
        echo '<pre style="font-size: 20px;">';
        echo 'PHP: ' . phpversion() . '<br><br>';

        $this->a1[] = function() { echo 'a1 <br/>'; };
        $this->a1[] = array($this, 'g');
        foreach ($this->a1 as $key => &$v)
        {
           //lets get the key that the internal pointer is pointing to 
           // before the call.
                  $intPtr = (key($this->a1) === null) ? 'null' : key($this->a1);
                echo 'array ptr before key ', $key, ' func call is ',    
                       $intPtr, '<br/>' ;
            call_user_func($v);
            //echo 'count(v) = ' . count($v) . ' ';
        }

        echo '<br><br>';

        // The same thing works fine with a for() loop.
        $this->a2[] = function() { echo 'a2 '; };
        $this->a2[] = array($this, 'h');
        for ($i = 0; $i < count($this->a2); ++$i)
            call_user_func($this->a2[$i]);

        echo '<br><br>';

        // It also works fine using a local array as long as it
        // starts off with more than one element.
        $a3[] = function() { echo 'a3 '; };
        //$a3[] = function() { echo 'a3 '; };
        $i = 0;
        foreach ($a3 as &$x)
        {
            call_user_func($x);
            if ($i++ > 1) // prevent infinite loop
                break;

            // Why does this get called only if $a3 originally starts
            // with more than one element?
            $a3[] = function() { echo 'callback '; };
        }

        echo '</pre>';
    }

    private function g()
    {
        echo 'g() <br>';
        $this->a1[] = function() { echo 'callback '; };
    }

    private function h()
    {
        echo 'h() <br>';
        $this->a2[] = function() { echo 'callback '; };
    }
}

$foo = new Foo;
$foo->f(); 

Output:

array ptr before key 0 func call is 1
a1 
array ptr before key 1 func call is null <-will not iterate over any added elements!
g() 

a2 h() 
callback 

a3

2.Why does modifying $a3 work during the third loop foreach ($a3 as &$x), but only when the array starts off with more than one element?

Of course if you add an element to the array before the internal pointer returns null then the element will be iterated over. In your case if the array has one element then on the first iteration the internal pointer has already returned null. However, if there is initially more than one element then the additional element can be added on the first iteration as the internal pointer will be pointing to the second intial element at this time.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.