PHPのアクセス権キーワード `private` と疑似変数 `$this` の落とし穴


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 メソッドを同じスコープからコールしようとする

protectedpublicは予想通りの結果になるので、特筆すべきところはないのですが、困ったことに、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なメンバーを参照する。

これでだいぶ迷いが消えました。よかったよかった〜


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

ABOUTこの記事をかいた人

Hi, 中国四川出身の王です。2008年に日本に渡り、大学卒業後Web制作会社勤務を経て、現在はフリーランスとしてゆるりと働いています。サイト制作の全般を担当しています。好きな生き物はプーティ(マイCat)です。趣味はアニメ鑑賞です。画家になるのが夢だったりします!