【メモ】PHPのstatic:: (遅延静的束縛)とは何なのか？


Laravelのサービスコンテナの仕組みを知りたい初心者です。
Laravelのサービスコンテナのコンストラクタを追っていたらいきなり`static::`を使っているコードが出てきました。`static::`ってなんだ？？？？
```php:\vendor\laravel\framework\src\Illuminate\Foundation\Application.phpから抜粋
    protected function registerBaseBindings()
    {
        // static::ってなんだ？？？？
        static::setInstance($this);

        $this->instance('app', $this);

        $this->instance(Container::class, $this);
        $this->singleton(Mix::class);

        $this->singleton(PackageManifest::class, function () {
            return new PackageManifest(
                new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
            );
        });
    }
```
今回は`static::`が何を表しているのかを調べてみました。どうやら遅延静的束縛とかいう仰々しい名前のやつ見たいです。
遅延静的束縛を理解するにはまず`::`(スコープ演算子)が何なのかを知っておく必要がありそうです。

# スコープ演算子(::)とは

> static, 定数 およびオーバーライドされたクラスのプロパティやメソッドにアクセスすることができます。
https://www.php.net/manual/ja/language.oop5.paamayim-nekudotayim.php

そしてスコープ演算子には非転送コールと転送コールの2種類があるみたいです。

# 非転送コール

クラス名が明示されたコール
```php:スコープ定義演算子のドキュメントから引用
<?php
class MyClass {
    const CONST_VALUE = 'A constant value';
}
$classname = 'MyClass';
echo $classname::CONST_VALUE;
echo MyClass::CONST_VALUE;
?>
```

# 転送コール

self、static、parentによるクラス名が明示されていないコール
selfは自クラスの静的メンバーなどにアクセスする際に使用します。$thisはインスタンスを表してるので、インスタンス化しないと使えないのでselfとはまた別物です。
parentは親クラスを表しています。
staticが今回の調査対象、遅延静的束縛を表しています。
```php:スコープ定義演算子のドキュメントから引用
<?php
class OtherClass extends MyClass
{
    public static $my_static = 'static var';
    public static function doubleColon() {
        echo parent::CONST_VALUE . "\n";
        echo self::$my_static . "\n";
    }
}
$classname = 'OtherClass';
$classname::doubleColon();
OtherClass::doubleColon();
?>
```

# 遅延静的束縛とは？
ここまで簡単にスコープ演算子について確認しました。
ここで遅延静的束縛について確認することができる最低限の知識が身につきました。
遅延静的束縛は「スコープ演算子」の「転送コール」の「static」のことを言っているようです。
遅延静的束縛は親クラスと子クラスで同じ名称のメンバーが存在する場合に、親クラスと子クラスどちらで定義したメンバーを参照するのかを指定するために使います。
遅延静的束縛では内部的に、直近の、非転送コールで使用されたクラス名で`static`を置き換えます。

遅延静的束縛のドキュメントに載っていたコードが分かりやすかったのでそちらに補足を書いて確認します。

https://www.php.net/manual/ja/language.oop5.late-static-bindings.php


# 例１：static:: のシンプルな使用法
一つ目はオーソドックスな使い方についてです。[1]～[3]の順で処理されます。
```php:遅延静的束縛のサンプル
<?php
class A {
    public static function who() {
        echo __CLASS__;
    }

    // [2]
    public static function test() {
        // 直前の非転送コールで明示したクラス名(B)で内部的に変換される為、
        // B::who()と同義になる。
        static::who(); // これで、遅延静的束縛が行われます
    }
}

class B extends A {
    // [3] Bと出力されます。
    public static function who() {
        echo __CLASS__;
    }
}


// [1]
// Bというクラスを明示している非転送コールです。
// Bにはtest関数がありませんが親クラスであるAに存在するのでそちらが呼び出されます。
B::test();
?>
```

# 例２：非静的コンテキストにおける static:: の使用法
二つ目は非静的メソッドがオーバーライドされている時、されていない時のサンプルです。
```php:遅延静的束縛のサンプル
<?php
class A {
    private function foo() {
        echo "success!\n";
    }
    public function test() {
        // $thisについてはAクラス内定義なのでA->foo()と同義
        // ここが落とし穴！！
        // これはAクラスに定義されているfooがprivateのときに限る。
        // 子クラス親クラス共に$thisで実行可能なメソッドがある場合、
        // 親クラスのprivateメソッドが優先される。
        // 基本的に呼び出し元のインスタンスを参照しているので、publicの時は呼び出し元(子インスタンス)のメソッドを実行する。
        $this->foo();
        // 直近の非転送コールがbの場合、Bにfoo()は定義されていない為、Aのfoo()が実行される
        // 直近の非転送コールがcの場合、Cにfoo()が定義されている為、Cのfoo()を実行しようとするが、
        // privateメソッドの為アクセスエラーが発生する。
        // ただしpublicに変更すればCのfoo()が実行できるのでエラーは発生しなくなる。
        static::foo();  // 静的メソッドであってもなくてもstatic::foo()のように記載する。
    }
}

class B extends A {
   /* foo() が B にコピーされるので、メソッドのスコープは A のままとなり、
    * コールは成功します */
}

class C extends A {
    private function foo() {
        /* もとのメソッドが置き換えられるので、新しいメソッドのスコープは C となります */
    }
}

// Bをインスタンスしていますが裏で親クラスのAもインスタンス化されています。
// そのためBクラス内での$thisはもちろんBですが、Aクラス内の$thisはAです。
$b = new B();
//非転送コール
$b->test();

$c = new C();
//非転送コール
$c->test();   //fails
?>
```
出力結果は以下のようになります。
```
success!
success!
success!
Fatal error:  Call to private method C::foo() from context 'A' in /tmp/test.php on line 9
```

# 例３：転送するコールと転送しないコール
最後は転送コールと非転送コールでそれぞれどんな動きになるのかを確認します。

```php:遅延静的束縛のサンプル
<?php
class A {
    public static function foo() {
        // A::foo();のときはAが直近の非転送コールの為、A::who()と同義
        // parent,self,staticのときは直近の非転送コールが、起点となった非転送コールの為、
        // C::who()と同義
        static::who();
    }

    public static function who() {
        echo __CLASS__."\n";
    }
}

class B extends A {
    // 非転送コールと転送コールで挙動を確認する
    public static function test() {
        A::foo();
        parent::foo();
        self::foo();
        static::foo();
    }

    public static function who() {
        echo __CLASS__."\n";
    }
}
class C extends B {
    public static function who() {
        echo __CLASS__."\n";
    }
}

// 起点となる非転送コール
// Cにはtestが実装されていないので親クラスのBのtestが呼び出される。
C::test();
?>
```
出力は以下になります。
```
A
C
C
C
```


参考資料
https://www.php.net/manual/ja/language.oop5.paamayim-nekudotayim.php
https://www.php.net/manual/ja/language.oop5.late-static-bindings.php
https://qiita.com/Tommy0221/items/235d6c4605fecce36c62