Duplicar las claves de la matriz (Aviso: la variable miembro "a" regresó de __sleep () varias veces)

8

El título puede parecer un poco tonto, pero estoy totalmente en serio con esto. Hoy en el trabajo me encontré con un comportamiento extraño de PHP que no podía explicar. Afortunadamente, este comportamiento se solucionó en PHP 7.4, por lo que parece que alguien también se topó con eso.

Hice un pequeño ejemplo para ilustrar lo que salió mal:

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

Ejecute este código aquí: https://3v4l.org/DBt3o

Aquí hay una pequeña explicación de lo que está sucediendo aquí. Tenemos las clases A y B, que comparten una propiedad $a. Los lectores cuidadosos notaron que la propiedad $atiene dos visibilidades diferentes (pública, privada). Nada lujoso hasta ahora. La magia ocurre en el __sleepmétodo que se llama mágicamente cuando hacemos serializenuestra instancia. Queremos tener todas las variables de objeto que se obtiene con get_object_varsreducir esto a sólo las teclas con array_keysy salida con todo var_dump.

Esperaría algo como esto (esto sucede desde PHP 7.4 y es mi salida esperada):

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

Pero lo que obtengo es esto:

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

¿Cómo podría ser que PHP entregue una matriz con dos claves completamente idénticas? ¿Quién puede explicar qué sucede aquí internamente porque en PHP simple no puedo generar una matriz con dos claves completamente idénticas? ¿O extraño algo obvio aquí?

Al principio, mis compañeros de trabajo no querían creerme, pero ninguno de ellos tuvo una buena explicación después de entender lo que está sucediendo aquí.

Realmente me encantaría ver una buena explicación.

Benjamin Paap
fuente
1
Es interesante si cambia la línea avar_dump(array_keys((array)$this));
Nigel Ren
Di una respuesta, pero desde entonces la eliminé porque ahora creo que dado este extracto del manual de PHP "Obtiene las propiedades no estáticas accesibles del objeto dado de acuerdo con el alcance", este es un error directo. Digo esto porque la propiedad ancestral privada $ a no es "accesible" para B. Supuse que este resultado puede deberse a que se refiere a $ this en A :: __ sleep y, por lo tanto, mostraba el alcance completo de todos, sin embargo tener lo movió a B :: __ sleep, el comportamiento sigue siendo idéntico.
Pancho

Respuestas:

6

No pude encontrar un informe para el error en la pregunta, pero curiosamente parece que este commit aborda lo mismo:

Si estamos en un ámbito donde la propiedad privada sombreada es visible, la propiedad pública sombreada no debería ser visible.

El código de prueba está bien escrito, con un simple cambio podríamos tenerlo aquí:

class Test
{
    private $prop = "Test";

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

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

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

Llamando var_dump()a $propsespectáculos:

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

De vuelta a su pregunta:

¿Cómo podría ser que PHP entregue una matriz con dos claves completamente idénticas? ¿Quién puede explicar qué sucede aquí internamente porque en PHP simple no puedo generar una matriz con dos claves completamente idénticas?

Sí, no puede tener una matriz con dos claves idénticas:

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

resultados en:

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

pero no me permita estar de acuerdo con usted two completely identical keysya que estos dos elementos con nombres de clave idénticos no se almacenan con claves idénticas internamente dentro de una tabla hash. Es decir, se almacenan como enteros únicos, excepto en posibles colisiones y, como esto ha estado ocurriendo internamente, se ignoró la restricción de las entradas del usuario.

revo
fuente
3

Después de jugar un poco con esto, parece que esto no depende __sleep().

Aparentemente, este siempre fue el caso en versiones anteriores de PHP 7 (pero aparentemente no en PHP 5). Este ejemplo más pequeño muestra el mismo comportamiento.

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

Salida de PHP 7.0 - 7.3

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

Creo que lo privado $aen el padre es una propiedad diferente al público $aen el niño. Cuando se cambia la visibilidad en Bque no va a modificar la visibilidad del $aen A, en realidad está haciendo una nueva propiedad con el mismo nombre. Si var_dumpel objeto en sí mismo puede ver ambas propiedades.

Sin embargo, no debería tener mucho efecto, ya que no podría acceder a la propiedad privada desde la clase principal en la clase secundaria, aunque puede ver que existe en esas versiones anteriores de PHP 7.

No se asuste
fuente
1
No debería ser posible que la matriz asociativa (tabla hash) esté en este estado. Accesible es solo uno de ellos, pero el tamaño es 2.
Weltschmerz
@Weltschmerz Estoy de acuerdo. Tiene un aspecto realmente extraño.
No entre en pánico el
2
Además, acceder al índice adevuelve el segundo This is $a from A.
AbraCadaver
@AbraCadaver También me di cuenta de eso. Supongo que esa parte tiene sentido, ya que terminarás con el último valor cuando escribas una matriz literal con claves duplicadas.
No entre en pánico el
0

Mi pareja centavos.

No sé sobre compañeros de trabajo, pero no creía y pensé que esto era una broma.

Para la explicación, definitivamente el problema está en la variable "get_object_vars", ya que devuelve una matriz asociativa duplicada. Debería haber dos valores de tabla hash diferentes para la misma clave (que no es posible, pero la única explicación viene). No pude encontrar ningún enlace a la implementación interna get_object_vars () (aunque PHP se basa en código abierto, por lo que es posible obtener código y depurar de alguna manera). También estoy pensando (sin éxito hasta ahora) en la forma de ver la representación de matriz en la memoria, incluida la tabla hash. Por otro lado, pude usar las funciones "legales" de PHP y hacer algunos trucos con la matriz.

Este es mi intento de probar alguna funcionalidad con esa matriz asociativa. A continuación se muestra la salida. No se requiere explicación: puede ver todo y probar el mismo código usted mismo, por lo que solo algunos comentarios.

  1. Mi entorno es php 7.2.12 x86 (32 bit) - bueno ... sí, lástima de mí

  2. Me deshago de la "magia" y la serialización, solo dejo cosas que traen problemas.

  3. Completó algunas refactorizaciones en las clases A y B, así como la llamada a la función.

  4. La clave $ bajo la clase A debe ser privada, de lo contrario no debe haber ningún milagro.

  5. Varias pruebas de piezas: nada interesante, excepto el problema principal.

  6. Prueba parcial copy_vars - ¡la matriz se copió con duplicado! La nueva clave se agregó con éxito.

  7. Parte de iteración de prueba y new_vars: la iteración pasó por duplicado sin problema, pero la nueva matriz no aceptó duplicado, la última clave fue aceptada.

  8. Reemplazo de prueba: reemplazo completado en la segunda clave, duplicación de la suspensión.

  9. Prueba de ksort: la matriz no cambió, no se reconoció el duplicado

  10. Prueba de clasificación: después de cambiar valores y ejecutar una clasificación, pude cambiar el orden e intercambiar claves duplicadas. Ahora la primera clave se convierte en la segunda y la nueva clave es la que llamamos matriz por clave o asignamos una clave. ¡Como resultado pude cambiar ambas teclas! Antes pensaba que la clave duplicada es invisible, ahora está claro que la última clave funciona cuando hacemos referencia o asignamos la clave.

  11. Conversión a objeto stdClass - ¡de ninguna manera! ¡Solo se acepta la última clave!

  12. Pruebas de desarmado: ¡buen trabajo! Última clave eliminada, pero la primera clave está a cargo y la única clave restante, sin duplicados.

  13. Prueba de representación interna: este es un tema para agregar algunas otras funciones y ver la fuente de duplicación. Estoy pensando en eso ahora.

El resultado resultante está debajo del 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);

Salida ahora:

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
fuente