0

When using PHP 5.5.9 and given this example test class:

class Test
  implements Iterator
{
  private $ar = [ 1, 2, 3 ];

  public function toArray() {
    return $this->ar;
  }

  public function rewind() {
    reset( $this->ar );
  }

  public function valid() {
    return !is_null( $this->key() );
  }

  public function key() {
    return key( $this->ar );
  }

  public function current() {
    return current( $this->ar );
  }

  public function next() {
    next( $this->ar );
  }
}

and executing this example test:

$t = new Test;
foreach( $t as $key => $value ) {
  echo "orig: $key: $value\n";

  // work on the immediate method result
  foreach( $t->toArray() as $copyKey => $copyValue ) {
    echo "  copy: $copyKey: $copyValue\n";
  }

  echo "----\n";
}

I get the following result:

orig: 0: 1
  copy: 0: 1
  copy: 1: 2
  copy: 2: 3
----

It appears as though the inner foreach loop is working on a reference to the internal member $ar of Test and advancing its internal pointer, when I expect $t->toArray() to give me a copy of the internal member $ar of Test.

Since my understanding was that PHP always copies arrays, certainly when returned from a method, I expected the following result:

orig: 0: 1
  copy: 0: 1
  copy: 1: 2
  copy: 2: 3
----
orig: 1: 2
  copy: 0: 1
  copy: 1: 2
  copy: 2: 3
----
orig: 2: 3
  copy: 0: 1
  copy: 1: 2
  copy: 2: 3
----

When I make an explicit copy of the array, before the inner foreach loop, with:

// store method result in variable first
$a = $t->toArray();
foreach( $a as $copyKey => $copyValue ) { /* etc. */ }

Or change the toArray() method to:

public function toArray() {
  $copy = $this->ar; // this alone won't work
  reset( $copy ); // this is what is needed to do the trick
  return $copy;
}

I do get the expected result.

What is going on here? Is this expected behavior? Am I overlooking something vital here?


Apparently this behavior disappeared in PHP 7.0.0:

https://3v4l.org/l96fW

Decent Dabbler
  • 22,532
  • 8
  • 74
  • 106
  • So what's the problem? Foreach does not use internal array pointer since v7.0. It's very well documented in http://php.net/manual/en/migration70.incompatible.php#migration70.incompatible.foreach and in http://php.net/manual/en/control-structures.foreach.php – Alex Blex Apr 19 '17 at 14:11
  • @Alex Blex The problem is that when I call `$t->toArray()`, I expect to get a copy of `$t->r`, not an apparent reference to the internal `Test` member. It changes the internal array pointer of a private member, which shouldn't be happening. This is unexpected behavior even before PHP 7, if I'm not mistaken. – Decent Dabbler Apr 19 '17 at 14:20
  • Yes, but it's been fixed, isn't it? – Alex Blex Apr 19 '17 at 14:33
  • Just trying to understand what kind of answer you expect here? – Alex Blex Apr 19 '17 at 14:42
  • @AlexBlex Whether this is expected behavior from PHP 5.5.9, or a known bug, or whether I may have overlooked something vital here and thus whether it might simply be a misconstrued example on my part. I mean, it's basically written out in my question. PS.: only after my initial post did it occur to me to test it on 3v4l.org, which appears to suggest it *is* indeed a bug. But as long as I can't find any concrete documentation/bug-report about it, I can't be sure. – Decent Dabbler Apr 19 '17 at 15:01
  • It was a long-standing issue with foreach using internal pointer. Fixed in v7 because it is a BC break. If you need to run your code in v5, assign the array to a local variable to de-reference it. I put the code snippet as an answer to make it a bit more readable. – Alex Blex Apr 19 '17 at 15:19

1 Answers1

0

Dereferencing of the array in v5.x:

$t = new Test;
foreach( $t as $key => $value ) {
    echo "orig: $key: $value\n";

    $a = $t->toArray(); // copy of array happens under `=` sign
    foreach( $a as $copyKey => $copyValue ) {
        echo "  copy: $copyKey: $copyValue\n";
    }

    echo "----\n";
}
Alex Blex
  • 34,704
  • 7
  • 48
  • 75
  • Thanks Alex. Just to expand a little more on my question: the reason for my confusion is two-fold. 1. I relied on the thought that a method would always return a copy of a scalar value or array. 2. I relied on a note in the English `foreach` documentation, that seems to have mysteriously vanished, as commenter [Alistair Hole points out](https://secure.php.net/manual/en/control-structures.foreach.php#114759), that said that `foreach` always acted on a copy of an array. Although, admittedly, that note *did* seemed to indicate that the array-pointer should nevertheless not be relied upon. – Decent Dabbler Apr 19 '17 at 15:40
  • The phrase in Alistairs' comment refers to the `key => value` pair, right to the `as` token, which **are** being copied by values, unless referenced. It has nothing to do with the array itself used left to the `as` token in the `foreach` construct. Wording of the paragraph is quite confusing, and indeed may lead to the wrong assumption that foreach uses a copy of the array itself. It's probably a good thing it's been removed. – Alex Blex Apr 19 '17 at 16:12
  • Wow, that's embarrassing. I've been developing PHP, on and off, for many years and it never occurred to me that *that* was what was being meant. Confusing, indeed. Although, in hindsight, it makes sense, of course, because one is able to alter the original array inside a `foreach` loop. I guess I just never gave it a critical enough thought. So, thank you for pointing that out. – Decent Dabbler Apr 19 '17 at 17:44
  • However, would you agree that it's still weird that `$t->toArray()` doesn't immediately return a copy of the array, no matter how it's called? Does that have anything to do with "copy-on-write" perhaps or is it merely related to the `foreach` internal pointer issue you mentioned? Or are both related, by any chance? – Decent Dabbler Apr 19 '17 at 17:46
  • I'd wish it was the only weird thing in php. My favourite one is DateTime object. On the bright site, inconsistency and unpredictability of the language forces tdd to confirm the code does what it seems it is doing. E.g. I'd never say `$a` is 0 after `$a = 2; $a = 2 - $a++;` unless I unttested it ;) – Alex Blex Apr 19 '17 at 21:17