Дублирующиеся ключи массива (Примечание: переменная-член "a" возвращается из __sleep () несколько раз) - PullRequest
7 голосов
/ 05 ноября 2019

Название может показаться немного глупым, но я абсолютно серьезно отношусь к этому. Сегодня на работе я натолкнулся на странное поведение PHP, которое я не мог объяснить. К счастью, это поведение исправлено в PHP 7.4, поэтому кажется, что кто-то тоже наткнулся на это.

Я сделал небольшой пример, чтобы проиллюстрировать, что пошло не так:

<?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);

Запустите этот код здесь: https://3v4l.org/DBt3o

Вот небольшое объяснение того, что здесь происходит. У нас есть классы A и B, которые оба имеют свойство $a. Внимательные читатели заметили, что свойство $a имеет две разные видимости (публичная, частная). Пока ничего особенного. Магия происходит в методе __sleep, который волшебным образом вызывается, когда мы serialize делаем наш экземпляр. Мы хотим, чтобы все переменные объекта, которые мы получаем с помощью get_object_vars, уменьшили это значение до ключей с array_keys и вывели все с помощью var_dump.

Я ожидал бы что-то подобное (это происходит с PHP 7.4). и это мой ожидаемый результат):

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

Но что я получаю, это:

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

Как может быть, что PHP предоставляет массив с двумя полностью идентичными ключами? Кто может объяснить, что здесь происходит внутри, потому что в простом PHP я не могу создать массив с двумя полностью идентичными ключами? Или я что-то упускаю здесь очевидное?

Мои коллеги сначала не хотели мне верить, но ни у кого из них не было хорошего объяснения этому после того, как они поняли, что здесь происходит.

Я действительнохотел бы увидеть хорошее объяснение.

Ответы [ 3 ]

4 голосов
/ 08 ноября 2019

Я не смог найти отчет об ошибке в вопросе, но интересно, что этот коммит обращается к тому же:

Если мы находимся в области видимости, гдескрытое приватное свойство является видимым, теневое открытое свойство не должно быть видимым.

Тестовый код хорошо написан, с простым изменением, которое мы могли бы получить здесь:

class Test
{
    private $prop = "Test";

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

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

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

Вызов var_dump() на $props показывает:

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

Возвращаясь к вашему вопросу:

Как может быть, что PHP предоставляет массив с двумя полностью идентичными ключами? Кто может объяснить, что здесь происходит внутри, потому что в простом PHP я не могу создать массив с двумя полностью идентичными ключами?

Да, вы не можете иметь массив с двумя одинаковыми ключами:

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

приводит к:

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

, но позвольте мне не согласитьсяс вами на two completely identical keys, так как эти два элемента с одинаковыми именами ключей не хранятся внутри идентичных ключей внутри хеш-таблицы. То есть они хранятся в виде уникальных целых чисел, за исключением возможных коллизий, и поскольку это происходило внутри, ограничение на вводимые пользователем данные было проигнорировано.

2 голосов
/ 05 ноября 2019

Немного поиграв с этим, похоже, что это не зависит от __sleep().

Очевидно, это всегда имело место в более ранних версиях PHP 7 (но, очевидно, не в PHP 5). Этот меньший пример демонстрирует то же поведение.

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());

Вывод из PHP 7.0 - 7.3

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

Я думаю, что private $a в родительском объекте отличается от public $a у ребенка. Когда вы изменяете видимость в B, вы не изменяете видимость $a в A, вы действительно создаете новое свойство с тем же именем. Если вы var_dump сам объект, вы можете видеть оба свойства.

Это не должно иметь большого эффекта, поскольку вы не сможете получить доступ к частному свойству из родительского класса в дочернем классе,хотя вы можете видеть, что он существует в этих более ранних версиях PHP 7.

0 голосов
/ 08 ноября 2019

Моя пара центов.

Я не знаю о сотрудниках, но я не верил и думал, что это шутка.

Для объяснения - определенно проблема в переменной "get_object_vars", так как она возвращает дублированный ассоциативныймассив. Должны быть два разных значения хеш-таблицы для одного и того же ключа (что невозможно, но есть единственное объяснение). Мне не удалось найти никаких ссылок на внутреннюю реализацию get_object_vars () (хотя PHP основан на открытом исходном коде, поэтому можно как-то получить код и отладить). Также я думаю (пока безуспешно) о том, как увидеть представление массива в памяти, включая хэш-таблицу. С другой стороны, я смог использовать PHP «легальные» функции и выполнить некоторые трюки с массивом.

Это моя попытка проверить некоторые функциональные возможности с этим ассоциативным массивом. Ниже вывод. Никакого объяснения не требуется - вы можете увидеть все и попробовать один и тот же код самостоятельно, поэтому только некоторые комментарии.

  1. Моя среда php 7.2.12 x86 (32 бит) - ну ... даПозор мне

  2. Я избавился от "магии" и сериализации, оставил только материал, вызывающий проблему.

  3. Завершен некоторый рефакторинг для классов Aи B, а также вызов функции.

  4. Ключ $ в классе A должен быть закрытым, иначе не будет чуда.

  5. Часть тестирования vars -ничего интересного, кроме основной проблемы.

  6. Проверка части copy_vars - массив был скопирован с дубликатом !! Новый ключ был успешно добавлен.

  7. Итерация проверки детали и new_vars - итерация прошла без дублирования, но новый массив не принял дубликат, последний ключ принят.

  8. Замена тестирования - замена завершена для второго ключа, повторное пребывание.

  9. Проверка ksort - массив не изменился, дубликат не был распознан

  10. Тестирование asort - после изменения значений и запуска asort я смог изменить порядок и заменить дубликаты ключей. Теперь первый ключ становится вторым, а новый - тем, когда мы вызываем массив по ключу или назначаем ключ. В результате я смог изменить оба ключа! Прежде, чем я думал, что дублирующий ключ является своего рода невидимым, теперь ясно, что последний ключ работает, когда мы ссылаемся или назначаем ключ.

  11. Преобразование в объект stdClass - никак! Только последний принятый ключ!

  12. Тестирование на отсутствие - хорошая работа! Последний ключ удален, но первый ключ отвечает, и остался только один ключ, без дубликатов.

  13. Внутренний тест представления - это предмет для добавления некоторых других функций и просмотра источникадублированияЯ думаю об этом сейчас.

Результат вывода ниже кода.

<?php

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

    protected function funcA() {
        $vars = get_object_vars($this);

        return $vars;
    }
}

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

    public function funcB() {
        return $this->funcA();
    }
}

$b = new B();

$vars = $b->funcB();

echo "testing vars:\n\n\n";

var_dump($vars);
var_dump($vars['key']);
var_dump(array_keys($vars));

echo "\n\n\ntesting copy_vars:\n\n\n";

$copy_vars = $vars;
$copy_vars['new_key'] = 'this is a new key';

var_dump($vars);
var_dump($copy_vars);

echo "\n\n\ntesting iteration and new_vars:\n\n\n";

$new_vars = [];
foreach($vars as $key => $val) {
    echo "adding '$key', '$val'\n";
    $new_vars[$key] = $val;
}

var_dump($new_vars);

echo "\n\n\ntesting replace key (for copy):\n\n\n";

var_dump($copy_vars);
$copy_vars['key'] = 'new key';
var_dump($copy_vars);

echo "\n\n\ntesting key sort (for copy):\n\n\n";

var_dump($copy_vars);
ksort($copy_vars);
var_dump($copy_vars);

echo "\n\n\ntesting asort (for copy):\n\n\n";

$copy_vars['key'] = "A - first";
var_dump($copy_vars);
asort($copy_vars);
var_dump($copy_vars);
$copy_vars['key'] = "Z - last";
var_dump($copy_vars);

echo "\n\n\ntesting object conversion (for copy):\n\n\n";

var_dump($copy_vars);
$object = json_decode(json_encode($copy_vars), FALSE);
var_dump($object);


echo "\n\n\ntesting unset (for copy):\n\n\n";

var_dump($copy_vars);
unset($copy_vars['key']);
var_dump($copy_vars);


echo "\n\n\ntesting inernal representation:\n\n\n";

debug_zval_dump($vars);

Вывод сейчас:

testing vars:


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



testing copy_vars:


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



testing iteration and new_vars:


adding 'key', 'This is $a from B'
adding 'key', 'This is $a from A'
array(1) {
  ["key"]=>
  string(17) "This is $a from A"
}



testing replace key (for copy):


array(3) {
  ["key"]=>
  string(17) "This is $a from B"
  ["key"]=>
  string(17) "This is $a from A"
  ["new_key"]=>
  string(17) "this is a new key"
}
array(3) {
  ["key"]=>
  string(17) "This is $a from B"
  ["key"]=>
  string(7) "new key"
  ["new_key"]=>
  string(17) "this is a new key"
}



testing key sort (for copy):


array(3) {
  ["key"]=>
  string(17) "This is $a from B"
  ["key"]=>
  string(7) "new key"
  ["new_key"]=>
  string(17) "this is a new key"
}
array(3) {
  ["key"]=>
  string(17) "This is $a from B"
  ["key"]=>
  string(7) "new key"
  ["new_key"]=>
  string(17) "this is a new key"
}



testing asort (for copy):


array(3) {
  ["key"]=>
  string(17) "This is $a from B"
  ["key"]=>
  string(9) "A - first"
  ["new_key"]=>
  string(17) "this is a new key"
}
array(3) {
  ["key"]=>
  string(9) "A - first"
  ["key"]=>
  string(17) "This is $a from B"
  ["new_key"]=>
  string(17) "this is a new key"
}
array(3) {
  ["key"]=>
  string(9) "A - first"
  ["key"]=>
  string(8) "Z - last"
  ["new_key"]=>
  string(17) "this is a new key"
}



testing object conversion (for copy):


array(3) {
  ["key"]=>
  string(9) "A - first"
  ["key"]=>
  string(8) "Z - last"
  ["new_key"]=>
  string(17) "this is a new key"
}
object(stdClass)#2 (2) {
  ["key"]=>
  string(8) "Z - last"
  ["new_key"]=>
  string(17) "this is a new key"
}



testing unset (for copy):


array(3) {
  ["key"]=>
  string(9) "A - first"
  ["key"]=>
  string(8) "Z - last"
  ["new_key"]=>
  string(17) "this is a new key"
}
array(2) {
  ["key"]=>
  string(9) "A - first"
  ["new_key"]=>
  string(17) "this is a new key"
}



testing inernal representation:


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