PHP、クラスメソッド内の静的変数、および1つのバグの履歴

一般的に、私はフロントエンド開発者です。 ただし、サーバー側で作業する必要がある場合があります。 私たちには小さなチームがあり、すべてが 本当の バックエンドプログラマは忙しいため、自分でメソッドを実装する方が高速です。 また、コミットを前後に移動する時間を無駄にしないために、時々一緒に座ってタスクに取り組みます。 最近、これらのペアプログラミングのラウンドの1つで、チームメイトと私はバグに遭遇しました。









それで、昼食後、同僚のローマのパルパラクに行ったとき、彼はユニットテストの整理を終えて、パック全体を立ち上げました。 テストの1つが例外をスローし、落ちました。 ええ、私たちは今、バグを修正すると思いました。 パッケージの外でテストを単独で実行し、成功しました。







午後の昼寝をする前に、Codeceptionをさらに数回実行しました。 パッケージでは、テストが落ち、単独で合格し、パッケージで落ちました...







私たちはコードに乗り込みました。







エンティティオブジェクトをクライアントに送信するための配列に変換するメソッドから、 Call to private method



クラッシュします。 最近、このプロセスのメカニズムは少し変更されましたが、すべてのクラスがリファクタリングされたわけではないため、メソッドは必須フィールドのリストを返すメソッド(これは古い方法です)が子クラスでオーバーライドされているかどうかを確認します。 そうでない場合、フィールドのリストはリフレクションによって形成され(これは新しい方法です)、対応するゲッターが呼び出されます。 この例では、ゲッターの1つがプライベートとして宣言されており、したがって、基本クラスからアクセスできません。 すべて次のようになります。







本質に焦点を合わせるための少し簡素化されたコード
 abstract class AbstractEntity { /*   */ public function toClientModel() { static $isClientPropsOriginal = null; if ($isClientPropsOriginal === null) { $reflector = new \ReflectionMethod($this, 'getClientProperties'); $isClientPropsOriginal = $reflector->getDeclaringClass()->getName() === 'AbstractEntity'; } if ($isClientPropsOriginal) { // TODO       return $this->toClientModelNew($urlGenerator); } $result = []; foreach ($this->getClientProperties() as $clientKey => $property) { $value = call_user_func([$this, 'get' . ucfirst($property)]); $result[$clientKey] = $this->formatValueForClient($value); } return $result; } public function toClientModelNew() { $result = []; /*    ,    ,    */ return $result; } public function getClientProperties() { /*     */ } /*   */ } class Advertiser extends AbstractEntity { /*   */ private $name; private function getName() { return $this->getCalculatedName(); } public function toClientModel() { $result = parent::toClientModel(); $result['name'] = $this->getName(); $result['role_id'] = $this->getRoleId(); return $result; } public function getClientProperties() { return array_merge(parent::getClientProperties(), [ 'role_id' => 'RoleId' /*      */ /*  name  ,     toClientModel */ ]); } /*   */ }
      
      





ご覧のとおり、リフレクターの結果はメソッド内の静的変数$isClientPropsOriginal



キャッシュされます。







-そして、何が、反射はそのような難しい操作ですか? 聞いた。

「うん、はい」とローマはうなずいた。







このクラスでは、反射のある行のブレークポイントはまったく機能しませんでした。 一度も。 静的変数はすでにtrue



に設定されており、インタープリターはtoClientModelNew



メソッドにtoClientModelNew



て落ちました。 割り当てが行われる場所を確認することをお勧めします。







 $isClientPropsOriginal = $reflector->getDeclaringClass()->getName() === 'AbstractEntity' ? get_class($this) : false;
      
      





変数$isClientPropsOriginal



"PaymentList"



が立っていました。 これは、 AbstractEntity



から継承された別のクラスで、2つの点で注目にgetClientProperties



ますgetClientProperties



メソッドをオーバーライドせず、既に少し前に動作した単体テストでテストされました。







-これはどうですか? 聞いた。 -継承中にメソッド内の静的変数が暴走しますか? では、なぜこれに気付かなかったのですか?







小説は私のものと同じくらい困惑していました。 私がコーヒーを飲みに行っている間、彼は私たちのクラス階層を模した小さなユニットテストをスケッチしましたが、彼は落ちませんでした。 何かが欠けていました。 静的変数は予期したとおりではなく、すべての場合で正しく動作しなかったため、理由を理解できませんでした。 「クラスメソッド内のphp静的変数」のグーグルは、静的変数が良くないことを除いて、何も良いことをしませんでした。 まあ、当たり前!







さて、Romanはコーヒーを飲みに行き、思慮深くPHPサンドボックスを開いて、最も簡単なコードを書きました。







簡単な例1







 class A { function printCount() { static $count = 0; printf("%s: %d\n", get_class($this), ++$count); } } class B extends A { } $a = new A(); $b = new B(); $a->printCount(); // A: 1 $a->printCount(); // A: 2 $b->printCount(); // B: 1 $b->printCount(); // B: 2 $b->printCount(); // B: 3
      
      





どういうわけかそれは動作するはずです。 最も驚きの原則、すべてのもの。 しかし、結局のところ、 toClientModel



メソッド内で定義された静的変数があり、それは子クラスでオーバーライドされます。 しかし、次のように書くとどうなりますか:







簡単な例2







 class A { function printCount() { static $count = 0; printf("%s: %d\n", get_class($this), ++$count); } } class B extends A { function printCount() { parent::printCount(); } } $a = new A(); $b = new B(); $a->printCount(); // A: 1 $a->printCount(); // A: 2 $b->printCount(); // B: 3 $b->printCount(); // B: 4 $b->printCount(); // B: 5
      
      





「なんて奇妙だ」と私は思った。 しかし、ある種のロジックがあります。 2番目の場合、静的変数を含むメソッドはparent::



経由で呼び出され、終了しますか、親クラスからのインスタンスが使用されますか? しかし、この状況から抜け出す方法は? 私は頭をかいて、私の例に少し加えました:







簡単な例3







 class A { function printCount() { $this->doPrintCount(); } function doPrintCount() { static $count = 0; printf("%s: %d\n", get_class($this), ++$count); } } class B extends A { function printCount() { parent::printCount(); } } $a = new A(); $b = new B(); $a->printCount(); // A: 1 $a->printCount(); // A: 2 $b->printCount(); // B: 1 $b->printCount(); // B: 2 $b->printCount(); // B: 3
      
      





ここにある! ローマンが戻ってきたばかりで、私は自分自身に満足して、私の業績を実証しました。 彼は、PHPStormのキーボードでキーを数回押すだけで、静的変数を含むセクションを別のメソッドにリファクタリングすることができました。







 private function hasOriginalClientProps() { static $isClientPropsOriginal = null; if ($isClientPropsOriginal === null) { $reflector = new \ReflectionMethod($this, 'getClientProperties'); $isClientPropsOriginal = $reflector->getDeclaringClass()->getName() === 'AbstractEntity'; } return $isClientPropsOriginal; }
      
      





しかし、そこにありました! 私たちの間違いは続きました。 hasOriginalClientProps



と、 hasOriginalClientProps



メソッドhasOriginalClientProps



private



として宣言されていることに気付きました。私の例ではpublic



でした。 簡単なチェックでは、 protected



public



ものが機能しており、 private



いないことprivate



示されました。







簡単な例4







 <?php class A { function printCount() { $this->doPrintCount(); } private function doPrintCount() { static $count = 0; printf("%s: %d\n", get_class($this), ++$count); } } class B extends A { function printCount() { parent::printCount(); } } $a = new A(); $b = new B(); $a->printCount(); // A: 1 $a->printCount(); // A: 2 $b->printCount(); // B: 3 $b->printCount(); // B: 4 $b->printCount(); // B: 5
      
      





最後に、 hasOriginalClientProps



メソッドがprotected



と宣言し、長いコメントを提供しました。







分析



時間は待たず、次のタスクに進みましたが、それでもこの動作は不可解でした。 PHPがこのように動作する理由を理解することにしました。 ドキュメンテーションは、あいまいなヒント以外は何も掘り下げませんでした。 以下では、 PHP Internals BookPHP Wikiソースコードの研究、オブジェクトが他のプログラミング言語でどのように実装されているかについての思慮深い読書に基づいて、何が起こっているのかを再構築しようとします。







PHPインタープリター内の関数はop_array



構造によって記述されます。この構造には、 とりわけ 、この関数の静的変数を含むハッシュテーブルが含まれます。 継承中に 、静的変数がない場合、関数は子クラスで再利用され、ある場合、メソッド内の子クラスが独自の静的変数を持つように複製が作成されます。







これまでのところは良いですが、 parent::printCount()



を介して親メソッドを呼び出すと、もちろん、その静的変数で動作する親クラスのメソッドに入ります。 したがって、例2は機能せず、例1は機能します。 そして、例3のように、静的変数を別のメソッドに削除すると、後のバインディングが役立ちます: A::printCount



メソッドは、クラスB



からA::doPrintCount



コピーを呼び出します(もちろん、元のA::doPrintCount



と同じです)。







個人的に、そのようなコピーは私にとってかなり重いように見えました。 どうやら、PHP開発者は同じように考え、プライベートメソッドのコピーを拒否しました。 結局のところ、それらはまだ子クラスと親クラスからは見えません! 見て、私たちはこのために物語の冒頭で致命的なものさえ見つけました。 したがって、プライベートメソッドはクラスの階層全体の単一のインスタンスに存在し、その中の静的変数も単一のコンテキストに存在します。 したがって、例4は機能しませんでした。







この動作は、シャギーな5.0.4以降、 サンドボックスで試したすべてのバージョンのPHPで繰り返されます。







では、プロジェクトのコードのバグが以前は感じられなかったのはなぜですか? どうやら、エンティティは異種グループによって作成されることはめったになく、作成された場合、同時にリファクタリングされました。 しかし、テストを実行すると、異なるメカニズムを介して動作する2つのオブジェクトが1つのスクリプトに実行され、一方が他方の状態を台無しにしました。







結論



(実際、すべての重大な記事には結論が必要です)







  1. 静的変数は悪です。

    まあ、それは、プログラミングの他の悪と同様に、慎重で思慮深いアプローチが必要です。 もちろん、隠された状態を使用していると批判することもできますが、慎重に使用すれば、非常に効果的なコードを書くことができます。 ただし、落とし穴は静的なものの後ろに隠れている可能性があります。 だから
  2. 単体テストを作成します。

    コード内の隠れた枠が別のリファクタリング後に出ないことを保証することはできません。 したがって、テスト可能なコードを作成し、テストでカバーします。 私が説明したのと同様のバグが、テストではなくバトルコードで発生した場合、1時間半から2時間ではなく、デバッグに1日かかります。
  3. 荒野に入ることを恐れないでください。

    静的変数のような単純なものでさえ、システムのドキュメントとPHPソースを深く掘り下げる言い訳として役立ちます。 そして、理解するためにそれらの何かさえ。


この感動的なメモで、私はあなたに別れを告げます。 この記事が、誰かが私たちが踏んだレーキを避けるのに役立つことを願っています。 ご清聴ありがとうございました!







PS:資料を準備するに貴重なアドバイスをしてくれたRoman parpalakに感謝します。








All Articles