Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Duplicate array keys (Notice: member variable "a" returned from __sleep() multiple times)

The title may seem a bit silly but I'm totally serious with this. Today at work I came across a weird PHP behaviour which I could not explain. Luckily this behaviour is fixed in PHP 7.4, so it seems that someone stumbled upon that, too.

I made a small example to illustrate what went wrong:

<?php

class A {
    private $a = 'This is $a from A';

    public $b = 'This is $b from A';

    public function __sleep(): array
    {
        var_dump(array_keys(get_object_vars($this)));

        return [];
    }
}

class B extends A
{
    public $a = 'This is $a from B';
}

$b = new B;

serialize($b);

Run this code here: https://3v4l.org/DBt3o

Heres a bit explanation of what is going on here. We have to classes A and B which both share a property $a. Careful readers noticed, that the property $a has two different visibilities (public, private). Nothing fancy so far. The magic happens in the __sleep method which gets magically called when we serialize our instance. We want to have all object variables which we get with get_object_vars reduce this to only the keys with array_keys and output everything with var_dump.

I would expect something like this (this happens since PHP 7.4 and is my expected output):

array(2) {
  [0]=>
  string(1) "b"
  [1]=>
  string(1) "a"
}

But what I get is this:

array(3) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
  [2]=>
  string(1) "a"
}

How could it be, that PHP delivers an array with two completely identical keys? Who is able to explain what happens here internally because in plain PHP I'm not able to generate an array with two completely identical keys? Or do I miss something obvious here?

My coworkers did not want to believe me at first but none of them had a good explanation of this after they understood what is happening here.

I really would love to see a good explanation.

like image 282
Benjamin Paap Avatar asked Nov 05 '19 19:11

Benjamin Paap


People also ask

How to remove duplicates in array in php?

The array_unique() function removes duplicate values from an array. If two or more array values are the same, the first appearance will be kept and the other will be removed. Note: The returned array will keep the first array item's key type.

How to get same value in array in php?

The array_intersect() function compares the values of two (or more) arrays, and returns the matches. This function compares the values of two or more arrays, and return an array that contains the entries from array1 that are present in array2, array3, etc.


2 Answers

I couldn't find a report for the bug in the question but interestingly it seems this commit addresses the same thing:

If we are in a scope where the shadowed private property is visible, the shadowing public property should not be visible.

The test code is well-written, with a simple change we could have it here:

class Test
{
    private $prop = "Test";

    function run()
    {
        return get_object_vars($this);
    }
}

class Test2 extends Test
{
    public $prop = "Test2";
}

$props = (new Test2)->run();

Calling var_dump() on $props shows:

array(2) {
  ["prop"]=>
  string(5) "Test2"
  ["prop"]=>
  string(4) "Test"
}

Back to your question:

How could it be, that PHP delivers an array with two completely identical keys? Who is able to explain what happens here internally because in plain PHP I'm not able to generate an array with two completely identical keys?

Yes, you are not able to have an array with two identical keys:

var_dump(array_flip(array_flip($props)));

results in:

array(1) {
  ["prop"]=>
  string(4) "Test"
}

but let me not agree with you on two completely identical keys as these two elements with identical key names aren't stored with identical keys internally within a hashtable. That is, those are stored as unique integers except on potential collisions and as this has been occurring internally the restriction on user inputs was ignored.

like image 127
revo Avatar answered Oct 20 '22 09:10

revo


After messing with this a bit, it looks like this doesn't depend on __sleep().

Apparently this was always the case in earlier versions of PHP 7 (but apparently not in PHP 5). This smaller example shows the same behavior.

class A {
    private $a = 'This is $a from A';

    public function showProperties() { return get_object_vars($this); }
}

class B extends A
{
    public $a = 'This is $a from B';
}

$b = new B;
var_dump($b->showProperties());

Output from PHP 7.0 - 7.3

array(2) {
  ["a"]=>
  string(17) "This is $a from B"
  ["a"]=>
  string(17) "This is $a from A"
}

I think the private $a in the parent is a different property than the public $a in the child. When you change the visibility in B you're not changing the visibility of the $a in A, you're really making a new property with the same name. If you var_dump the object itself you can see both properties.

It shouldn't have much effect though, since you wouldn't be able to access the private property from the parent class in the child class, even though you can see it exists in those earlier PHP 7 versions.

like image 27
Don't Panic Avatar answered Oct 20 '22 08:10

Don't Panic