Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP class: referenced variable in one method affects non-referenced variable in other method

I'm puzzled by the behaviour in the following example. I stumbled over this in a project and it took me hours to narrow the problem down to a simple example. So this is my simple test class:

<?php
class Foo {
    public $sess = [['name' => 'John']];

    public function info() {
        $this->runTest();
        echo $this->sess[0]['name'];
    }

    private function runTest() {
        $localSess = &$this->sess[0];
        $this->sessTest();
    }

    private function sessTest() {
        $sessCopy = $this->sess;
        $this->sess[0]['name'] = 'Bob';
        $this->sess = $sessCopy;
    }
}

$myFoo = new Foo;
$myFoo->info();

The unexpected resulting output is:

Bob

If $localSess is just a simple assignment and not a reference, the output is (as expected): John

I don't understand what's going on here. Since the class property $sess is an array and not an object, the assignment to $sessCopy should be a simple copy. But if you print the content of $sessCopy right after modifying $this->sess, it will contain the changed value for 'name'. So it seems as if $sessCopy were a reference after all? But only if $localSess in the calling method is a reference. Why should that even matter? BTW, if $localSess is a reference to the whole array of the class property and not just to its first index, everything works as expected.

Can someone please shed some light on this? If the mere existence of a variable reference in one function can influence the content of a local variable (not a reference!) in another function, then that seems dangerous and scary.

like image 928
Stoppeye Avatar asked Apr 07 '26 23:04

Stoppeye


1 Answers

So I decided to create a simpler example that illustrates the same problem:

$a = [['name' => 'John']];
$b = $a; // makes a copy of `$a`
$b[0]['name'] = "Bob";
echo $a[0]['name]; // returns "John" as expected

However if you did:

$a = [['name' => 'John']];
/* var_dump($a) returns:
array(1) {
  [0]=>
  array(1) {
    ["name"]=>
    string(4) "John"
  }
}
*/
$local = &$a[0]; // seems to modify $a[0], so its type &array
/* var_dump($a) returns:
array(1) {
  [0]=>
  &array(1) {
    ["name"]=>
    string(4) "John"
  }
}
*/
$b = $a; // makes a copy of `$a`
$b[0]['name'] = "Bob";
echo $a[0]['name]; // returns "Bob"

This behaviour I admit is odd, but is documented in multiple PHP doc comments:

  • https://www.php.net/manual/en/language.references.php#79220
  • https://www.php.net/manual/en/language.references.whatdo.php#102123
  • https://www.php.net/manual/en/language.references.php#79220

It turns out this is actually a PHP bug first reported in 2000:

  • https://bugs.php.net/bug.php?id=20993
  • https://bugs.php.net/bug.php?id=6417
  • https://bugs.php.net/bug.php?id=7412
  • https://bugs.php.net/bug.php?id=15025

There was supposed to be updates to the documentation but I could not find it:

Due to peculiarities of the internal workings of PHP, if a reference is made to a single element of an array and then the array is copied, whether by assignment or when passed by value in a function call, the reference is copied as part of the array. This means that changes to any such elements in either array will be duplicated in the other array (and in the other references), even if the arrays have different scopes (e.g. one is an argument inside a function and the other is global)! Elements that did not have references at the time of the copy, as well as references assigned to those other elements after the copy of the array, will behave normally (i.e. independent of the other array).

However, it has been marked as Wont fixed, likely due to performance reasons. Notable comments:

We have discussed this issue and it will put a considerable slowdown on php's performance, to fix this properly.

Therefore this behaviour will be documented.

Even though it was never documented.

I have decided to raise another bug report on this issue to see if the position on this issue has changed.

I have now re-reported the issue: https://bugs.php.net/bug.php?id=80955

Solutions

One workaround is:

$b = unserialize(serialize($a));

Another is:

function array_clone($array) {
    return array_map(function($element) {
        if ((is_array($element))) {
            return array_clone($element);
        } else if (is_object($element)) {
            return clone $element;
        } else {
            return $element;
        }
    }, $array);
}

$b = array_clone($a);
like image 66
Yahya Uddin Avatar answered Apr 09 '26 12:04

Yahya Uddin



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!