PHPのアクセス権キーワード private
と疑似変数 $this
、どちらも私が初心者の頃から慣れ親しんできたワードだし、日常的にもお世話になっているのです。
お恥ずかしい話ですが、つい最近までは実は分かるようで分からないでいたのです…!
何がよく分かってなかったかというとですね、クラスを継承した時に、継承したメソッド( public | protected )の中の$this
が指すオブジェクトは継承元のインスタンスなのか、それとも継承先のインスタンスなのかということです。
そして、継承できるはずもないprivate
なプロパティを継承したメソッド経由でアクセスできる辺りもなんだか不思議でならないです。
そろそろスッキリさせたいなと思いました…!
公式ドキュメントを読み漁っても詳しい説明が載ってなかったので、自分で実験してみて分かったことをまとめておきたいと思います。私と同じくここらへんで違和感を覚えている人たちのご参考になれば嬉しいです。
$this が指すオブジェクトについて
class A { protected function foo($b) { echo ($b === $this ? '$b IS $this' : '$b IS NOT $this') . " (in A)" . PHP_EOL; $class_of_this = get_class($this); echo "Calling private method in class === A ===. `\$this` is a instance of class === $class_of_this ===" . PHP_EOL; } } class B extends A { public function bar($b) { echo ($b === $this ? '$b IS $this' : '$b IS NOT $this') . " (in B)" . PHP_EOL; $this->foo($b); // クラス `A` から継承してきたメソッド `foo` をコールする } } $b = new B; $b->bar($b); // 自分自身を B::bar に渡す // 結果: // // $b IS $this (in B) // $b IS $this (in A) // Calling private method in class === A ===. `$this` is a instance of class === B ===
公式ドキュメントにもあるように「$this は呼び出し元オブジェクトへの参照である」。
B::bar
の中の $this
は、呼び出し元オブジェクトである 「クラスの外の $b」への参照。
A::foo
の中の $this
は、呼び出し元オブジェクトである「クラスBの中の $this」への参照。
つまり B::bar
の中の $this
も、A::foo
の中の $this
も、外側の $b
である(への参照)ことが分かる。
もっと言えば、クラスの継承がどんなに深くても、$this
は常に最初(ルート)の呼び出し元オブジェクトである。
落とし穴:クラスAの中の $this
とクラスBの中の $this
が違うものだと考える。
正解:クラスAの中の $this
とクラスBの中の $this
が同一のオブジェクト(クラスBのインスタンス)である。
基本的$this
が表すオブジェクトは終始変化しないと考えて問題ないのですが、例外もある。
$this は、呼び出し元オブジェクト (通常は、メソッドが属するオブジェクトですが、 メソッドが第二のオブジェクトのオブジェクトの コンテキストから スタティックに コールされる場合には、別のオブジェクトとなる場合もあります) への参照です。
そもそもそういう使い方は非推奨だし、将来的に廃止する予定なので、無視していいレベルですがな。
privateなメンバーと実行コンテキストの関係
<?php ini_set('display_errors', 1); abstract class A { private $private_var = 'private var in A'; private $private_var_that_only_A_has = 'private var that only A has'; protected $protected_var = 'protected var in A'; protected $protected_var_to_be_inherited = 'protected var to be inherited from A'; protected function foo() { global $b; if ($b instanceof B AND $b === $this) { echo '$b is a instance of B, and $b identical to $this' . PHP_EOL; } else { return; } // ここから先は、`$this === $b` という前提で話が進む (つまり、"呼び出し元オブジェクト"が クラスBのインスタンスである`$b`) echo $this->protected_var . PHP_EOL; // => 'protected var in B' // // $b->protected_var だと考えれば納得だよね! echo $this->protected_var_to_be_inherited . PHP_EOL; // => 'protected var to be inherited from A' // // $b->protected_var_to_be_inherited だと考えれば納得だよね! // B::protected_var_to_be_inherited なんていうプロパティは定義されていないが、 // クラスAから継承しているのだから持っているのも同然。 echo $this->private_var . PHP_EOL; // => 'private var in A' // // $b->private_var だと考えれば 'private var in B' になりそうだが… はて…!? echo $this->private_var_that_only_A_has . PHP_EOL; // => 'private var that only A has' // // $b->private_var_that_only_A_has だと考えれば不自然だよね? // クラスBは A::private_var_that_only_A_has を継承していないのに、アクセスできる。 // ということは、privateな プロパティ/メソッド であっても、 // 本当は継承はされるけど、実行時のコンテキストが // その プロパティ/メソッド を定義したクラス(この場合クラスA)である場合のみアクセスできる。 $this->baz(); // => 'baz in A (private method)' // // $b->baz() しているのにも関わらず、実際は A::baz() が呼ばれた! // やはりこの挙動の原因は「実行時のコンテキストがクラスA」だと思われる。 $this->foobar(); // => 'foobar in B (protected method)' // // $b->foobar() だと考えれば納得だよね! // クラスB は クラスA の foobar メソッドを上書きしたから、当然の結果だね。 $this->qux(); // => Fatal error: Call to private method B::qux() from context 'A' // $b->qux() だと考えればコールできそうだが、実際はエラー! // 実行時のコンテキストが 'A' だから、B::qux() にはアクセスできないらしい。 // * ====== 結論 ====== * // privateな メンバー(メソッド/プロパティ) を参照する時、 // 実行時のコンテキスト内( $this が書かれたクラス内 )の メソッド/プロパティ を優先して参照しようとするクセがある。 // 言い換えれば、privateな メンバーにアクセスできるかどうかは、実行時のコンテキストで決まる。 } private function baz() { echo "baz in A (private method)" . PHP_EOL; } protected function foobar() { echo "foobar in A (protected method)" . PHP_EOL; } } class B extends A { private $private_var = 'private var in B'; protected $protected_var = 'protected var in B'; public function bar() { $this->foo(); } private function baz() { echo "baz in B (private method)" . PHP_EOL; } protected function foobar() { echo "foobar in B (protected method)" . PHP_EOL; } private function qux() { echo "qux in B (private method)" . PHP_EOL; } } $b = new B; $b->bar(); // 実行結果: // // $b is a instance of B, and $b identical to $this // protected var in B // protected var to be inherited from A // private var in A // private var that only A has // baz in A (private method) // foobar in B (protected method) // // Fatal error: Call to private method B::qux() from context 'A'
この実験で分かったこと
$this
が参照するメンバーがprivate
の場合、常に実行時のコンテキスト内(その$this
が書かれたクラス内)のprivate
なメンバーを参照する。
言い換えれば、privateな メンバーにアクセスできるかどうかは、実行時のコンテキストで決まる。
本当にちらっとだけですが、公式ドキュメントにも、この実験結果を裏付ける記述があります。
$this-> は private メソッドを同じスコープからコールしようとする
protected
とpublic
は予想通りの結果になるので、特筆すべきところはないのですが、困ったことに、static::によるprivateなメンバーへのアクセスの場合はまた挙動が違います… 詳しくはこちらの記事をご覧下さい。
PHPの「遅延静的束縛 (Late Static Bindings)」機能、解読!
もうちょっと拡張
上のサンプルコードにもあるように、$this
の実体は「とあるクラスのインスタンス」です。それが分かれば、こんなことができるのも納得がいくはずです。
<?php class A { private $private_var = 'private_var in A'; public function set_private_var(A $object, $value) { // 渡されたオブジェクトの private なプロパティを設定している // もちろん、これができる前提はその渡されたオブジェクトのタイプが `A` であること。 $object->private_var = $value; } public function print_private_var() { echo $this->private_var . PHP_EOL; } } class B extends A { } $b = new B; $b->print_private_var(); // => 'private_var in A' $b->set_private_var($b, 'new value!'); $b->print_private_var(); // => 'new value!'
まとめ
$this
は常にルートの呼び出し元オブジェクトである。$this
が参照するメンバーがprivate
の場合、常に実行時のコンテキスト内(その$this
が書かれたクラス内)のprivate
なメンバーを参照する。
これでだいぶ迷いが消えました。よかったよかった〜