Chaves de matriz duplicadas (aviso: a variável de membro “a” retornou de __sleep () várias vezes)

8

O título pode parecer um pouco bobo, mas eu sou totalmente sério com isso. Hoje no trabalho, me deparei com um comportamento estranho do PHP que não consegui explicar. Felizmente, esse comportamento foi corrigido no PHP 7.4, então parece que alguém se deparou com isso também.

Fiz um pequeno exemplo para ilustrar o que deu errado:

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

Execute este código aqui: https://3v4l.org/DBt3o

Aqui está uma pequena explicação do que está acontecendo aqui. Temos as classes A e B, que compartilham uma propriedade $a. Os leitores cuidadosos notaram que a propriedade $atem duas visibilidades diferentes (pública, privada). Nada extravagante até agora. A mágica acontece no __sleepmétodo que é chamado magicamente quando usamos serializenossa instância. Queremos ter todas as variáveis ​​de objeto com as quais get_object_varsreduzimos isso apenas para as chaves array_keyse produzimos tudo com var_dump.

Eu esperaria algo assim (isso acontece desde o PHP 7.4 e é a minha saída esperada):

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

Mas o que eu recebo é o seguinte:

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

Como poderia ser, que o PHP oferece uma matriz com duas chaves completamente idênticas? Quem é capaz de explicar o que acontece aqui internamente porque, em PHP simples, não sou capaz de gerar uma matriz com duas chaves completamente idênticas? Ou eu sinto falta de algo óbvio aqui?

Meus colegas de trabalho não quiseram acreditar em mim a princípio, mas nenhum deles teve uma boa explicação disso depois de entenderem o que está acontecendo aqui.

Eu realmente adoraria ver uma boa explicação.

Benjamin Paap
fonte
1
É interessante se você alterar a linha para #var_dump(array_keys((array)$this));
Nigel Ren
Eu dei uma resposta, mas já a removi, porque agora acho que, dado esse trecho do manual do PHP "Obtém as propriedades não estáticas acessíveis do objeto especificado de acordo com o escopo". Este é um erro simples. Digo isso porque a propriedade ancestral privada $ a não é "acessível" a B. Supus que esse resultado possa ser porque você se refere ao $ this em A :: __ sleep e, portanto, estava mostrando o escopo completo de todos, porém tendo movido para B :: __ sleep, o comportamento permanece idêntico.
Pancho

Respostas:

6

Não consegui encontrar um relatório para o bug na pergunta, mas curiosamente parece que esse commit aborda a mesma coisa:

Se estivermos em um escopo em que a propriedade privada sombreada é visível, a propriedade pública que está sombreada não deve estar visível.

O código de teste está bem escrito, com uma simples alteração, podemos tê-lo aqui:

class Test
{
    private $prop = "Test";

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

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

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

Chamando var_dump()na $propsmostra:

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

Voltar à sua pergunta:

Como poderia ser, que o PHP oferece uma matriz com duas chaves completamente idênticas? Quem é capaz de explicar o que acontece aqui internamente porque, em PHP simples, não sou capaz de gerar uma matriz com duas chaves completamente idênticas?

Sim, você não pode ter uma matriz com duas chaves idênticas:

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

resulta em:

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

mas não permita que você concorde com você, two completely identical keyspois esses dois elementos com nomes de chave idênticos não são armazenados internamente com chaves idênticas em uma hashtable. Ou seja, eles são armazenados como números inteiros exclusivos, exceto em possíveis colisões e, como isso ocorre internamente, a restrição nas entradas do usuário foi ignorada.

revo
fonte
3

Depois de mexer um pouco com isso, parece que isso não depende __sleep().

Aparentemente, esse sempre foi o caso nas versões anteriores do PHP 7 (mas aparentemente não no PHP 5). Este exemplo menor mostra o mesmo comportamento.

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

Saída do PHP 7.0 - 7.3

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

Eu acho que o privado $anos pais é uma propriedade diferente do público $ana criança. Quando você alterar a visibilidade em Bvocê não está alterando a visibilidade do $ano A, você está realmente fazendo uma nova propriedade com o mesmo nome. Se você for var_dumpo próprio objeto, poderá ver as duas propriedades.

Porém, não deve ter muito efeito, já que você não seria capaz de acessar a propriedade privada da classe pai na classe filho, mesmo que você possa ver que ela existe nessas versões anteriores do PHP 7.

Não entre em pânico
fonte
1
Não deve ser possível que a matriz associativa (tabela de hash) esteja nesse estado. Acessível é apenas um deles, mas o tamanho é 2.
Weltschmerz
@Weltschmerz Concordo. É realmente estranho.
Não se apavora o
2
Além disso, o acesso ao índice aretorna o segundo This is $a from A.
precisa saber é o seguinte
@AbraCadaver notei isso também. Suponho que essa parte faça sentido, pois você terminará com o último valor quando escrever um literal de matriz com chaves duplicadas.
Não se apavora o
0

Meus centavos.

Não conheço colegas de trabalho, mas não acreditei e pensei que isso fosse uma piada.

Para a explicação - definitivamente o problema está na variável "get_object_vars", pois está retornando um array associativo duplicado. Deve haver dois valores diferentes de tabela de hash para a mesma chave (o que não é possível, mas a única explicação vem). Não consegui encontrar nenhum link para a implementação interna de get_object_vars () (embora o PHP seja baseado em código aberto, é possível obter código e depurar de alguma forma). Também estou pensando (sem êxito até agora) no caminho para ver a representação da matriz na memória, incluindo tabela de hash. Por outro lado, eu era capaz de usar funções "legais" do PHP e fazer alguns truques com o array.

Esta é minha tentativa de testar algumas funcionalidades com essa matriz associativa. Abaixo está a saída. Nenhuma explicação é necessária - você pode ver tudo e experimentar o mesmo código, portanto, apenas alguns comentários.

  1. Meu ambiente é php 7.2.12 x86 (32 bits) - bem ... sim, que pena!

  2. Eu me livrei da "mágica" e da serialização, deixei apenas coisas trazendo problemas.

  3. Concluiu algumas refatorações nas classes A e B, bem como na chamada de função.

  4. A chave $ na classe A deve ser privada, caso contrário não será um milagre.

  5. Vars de teste de peças - nada de interessante, exceto o problema principal.

  6. Teste de peça copy_vars - o array foi copiado com duplicado !! Nova chave foi adicionada com sucesso.

  7. Iteração de teste de peça e new_vars - a iteração foi duplicada sem problemas, mas a nova matriz não aceitou a última chave duplicada e aceita.

  8. Substituição de teste - substituição concluída na segunda chave, estadia duplicada.

  9. Testando o ksort - o array não mudou, o duplicado não foi reconhecido

  10. Testando o asort - após alterar os valores e executar o asort, fui capaz de alterar a ordem e trocar chaves duplicadas. Agora, a primeira chave se torna a segunda e a nova chave é a que chamamos de matriz por chave ou atribuímos uma chave. Como resultado, consegui alterar as duas chaves !! Antes que eu pensasse que a chave duplicada é meio invisível, agora está claro que a última chave funciona quando fazemos referência ou atribuímos a chave.

  11. Conversão para objeto stdClass - de jeito nenhum! Somente a última chave aceita!

  12. Testando quanto à falta de ajuste - bom trabalho! Última chave removida, mas a primeira chave está no comando e a única chave restante, sem duplicatas.

  13. Teste de representação interna - este é um assunto para adicionar outras funções e ver a fonte da duplicação. Estou pensando nisso agora.

A saída do resultado está abaixo do código.

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

Saída agora:

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)
}
Anatoliy R
fonte