重複する配列キー(注意:メンバー変数「a」が__sleep()から複数回返されます)


8

タイトルは少しばかげているように見えるかもしれませんが、私はこれについて完全に真剣です。今日の仕事で、説明できない奇妙な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は2つの異なる可視性(パブリック、プライベート)があることに気づきました。今のところ何も変わっていない。魔法__sleepserialize、インスタンスのときに魔法のように呼び出されるメソッドで発生します。取得したすべてのオブジェクト変数に、これを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が2つの完全に同一のキーを持つ配列を提供するのはどうしてでしょうか?プレーンなPHPでは2つの完全に同一のキーを持つ配列を生成できないため、内部で何が起きているのか誰が説明できますか?または、ここで明らかな何かを見逃していますか?

私の同僚は最初は私を信じたくありませんでしたが、彼らがここで何が起こっているのかを理解した後、彼らの誰もこれをうまく説明できませんでした。

私は本当に良い説明を見たいです。


1
行を次のように変更すると興味深いですvar_dump(array_keys((array)$this));
Nigel Ren

私は答えを出しましたが、それを削除しました。これは、PHPマニュアルからこの抜粋を与えると、「スコープに従って、指定されたオブジェクトのアクセス可能な非静的プロパティを取得する」と考えるためです。これは簡単なバグです。これは、プライベートの祖先プロパティ$ aがBに「アクセス可能」ではないためです。この結果は、A :: __ sleepの$ thisを参照し、すべてのスコープをすべて表示しているためと考えられます。 B :: __ sleepに移動しましたが、動作は同じままです。
パンチョ

回答:


6

質問のバグに関するレポートは見つかりませんでしたが、興味深いことに、このコミットは同じこと扱っているようです:

シャドウされたプライベートプロパティが表示されるスコープ内にいる場合、シャドウされたパブリックプロパティは表示されません。

テストコードは適切に記述されており、簡単な変更を加えることで、次のようになります。

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が2つの完全に同一のキーを持つ配列を提供するのはどうしてでしょうか?プレーンなPHPでは2つの完全に同一のキーを持つ配列を生成できないため、内部で何が起きているのか誰が説明できますか?

はい、2つの同一のキーを持つ配列を持つことはできません。

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

結果は:

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

ただしtwo completely identical keys、同じキー名を持つこれらの2つの要素は、ハッシュテーブル内の同じキーとともに内部的に格納されないため、同意しません。つまり、衝突の可能性がある場合を除き、これらは一意の整数として保存されます。これは内部で発生しているため、ユーザー入力の制限は無視されました。


3

これを少しいじってみると、これはに依存していないよう__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"
}

$a親の私的なもの$aは、子供の公的なものとは異なる財産だと思います。であなたは可視性を変更するとB、あなたがの視界に変化していない$aではA、あなたが本当に同じ名前で新しいプロパティを作っています。var_dumpオブジェクト自体の場合は、両方のプロパティを確認できます。

ただし、以前のPHP 7バージョンに存在することが確認できても、子クラスの親クラスからプライベートプロパティにアクセスすることはできないため、あまり効果はありません。


1
連想配列(ハッシュテーブル)がこの状態である可能性はありません。アクセは、いずれか一方のみで、まだサイズは2です
Weltschmerz

@Weltschmerz同意する。それは本当に奇妙に見えます。
パニックにならないでください。

2
また、インデックスにアクセスするとa2番目のインデックスが返されますThis is $a from A
AbraCadaver

@AbraCadaver私もそれに気づきました。重複するキーを持つ配列リテラルを書くと、最後の値になるので、その部分は理にかなっていると思います。
パニックにならない

0

私のカップルのセント。

私は同僚については知りませんが、信じていなかったし、これは冗談だと思いました。

説明のために-複製された連想配列を返すため、間違いなく "get_object_vars"変数に問題があります。同じキーに対する2つの異なるハッシュテーブル値である必要があります(これは不可能ですが、唯一の説明があります)。内部のget_object_vars()実装へのリンクを見つけることができませんでした(PHPはオープンソースに基づいているため、コードを取得してデバッグすることは可能です)。また、ハッシュテーブルを含むメモリ内の配列表現を確認する方法について(今のところ失敗)と考えています。一方で、PHPの「正当な」関数を使用して配列を操作することができました。

これは、その連想配列でいくつかの機能をテストする私の試みです。以下は出力です。説明は必要ありません。すべてを見て、同じコードを自分で試すことができるため、コメントのみです。

  1. 私の環境はphp 7.2.12 x86(32ビット)です。

  2. 私は「マジック」とシリアライゼーションを取り除き、問題をもたらすものだけを残しました。

  3. クラスAおよびBのいくつかのリファクタリングと関数呼び出しを完了しました。

  4. クラスAの$ keyは秘密でなければなりません。そうでなければ奇跡はありません。

  5. パーツテスト変数-主な問題以外は何も興味深いものではありません。

  6. パーツテストcopy_vars-配列が重複してコピーされました!! 新しいキーが正常に追加されました。

  7. パートテストの反復とnew_vars-反復は問題なく複製されましたが、新しい配列は複製を受け入れず、最後のキーが受け入れられました。

  8. 交換のテスト-2番目のキーで交換が完了しました。重複して滞在します。

  9. ksortのテスト-配列は変更されず、重複は認識されませんでした

  10. asortのテスト-値を変更してasortを実行した後、順序を変更して重複キーを交換することができました。これで最初のキーが2番目になり、新しいキーは配列をキーで呼び出すか、キーを割り当てるときのものになります。その結果、両方のキーを交換することができました!! 重複するキーは目に見えないものだと思っていた前に、キーを参照または割り当てたときに最後のキーが機能することは明らかです。

  11. stdClassオブジェクトへの変換-方法はありません!最後のキーのみが受け入れられます!

  12. 未設定のテスト-よくできました!最後のキーは削除されましたが、最初のキーが担当し、キーは1つだけ残っています。重複はありません。

  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)
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.