chocolate Tech room

チョコを食べながら技術のことを考える

static:: による遅延静的束縛

という言語仕様を先日初めて認識したので、ざっくり調べてみました。

公式いわく

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

PHP 5.3.0 以降、PHP に遅延静的束縛と呼ばれる機能が搭載されます。

これを使用すると、静的継承のコンテキストで呼び出し元のクラスを参照できるようになります。

より正確に言うと、遅延静的束縛は直近の "非転送コール" のクラス名を保存します。…

驚くほど意味がつかめないですね。「非転送コール」ってなんやねん状態です。

さっと目を通してとりあえず static:: を使う場合に発揮されるなにか、ということは掴めたので、サンプルコードを書いていきましょう。

self::static:: の挙動の違い

まずは self:: の挙動を確認します。

<?php

class Animal
{
    public static int $legs = 4;

    public static function walk(): void
    {
        echo self::$legs. '本足で歩く';
    }
}

class Human extends Animal
{
    public static int $legs = 2;
}

Human::walk(); // => 4本足で歩く

self:: で参照されるのは、常に self:: が書かれたクラスになります。

そのため、継承先のクラスで static な変数やメソッドを定義し直しても、オーバーライドすることはできません

この制限を逃れるため使われるのが、 static:: による遅延静的束縛です。

では、 static:: の挙動を見ていきましょう。先程のコードを少し変更します。

<?php

class Animal
{
    public static int $legs = 4;

    public static function walk(): void
    {
        echo static::$legs. '本足で歩く';
    }
}

class Human extends Animal
{
    public static int $legs = 2;
}

Human::walk(); // => 2本足で歩く

変数の上書きができました! もちろん、static メソッドのオーバーライドも可能です。

実際に起こっていること

さて、遅延静的束縛が何をするものなのか、イメージはついたでしょうか?

もう少しだけ踏み込んで考えていきましょう。

公式ドキュメント を読み進めます。

静的メソッドの場合、これは明示的に指定されたクラス (通常は :: 演算子の左側に書かれたもの) となります。静的メソッド以外の場合は、そのオブジェクトのクラスとなります。 "転送コール" とは、self:: や parent::、static:: による静的なコール、 あるいはクラス階層の中での forward_static_call() によるコールのことです。 get_called_class() 関数を使うとコール元のクラス名を文字列で取得できます。 static:: はこのクラスのスコープとなります。

またややこしいですね。整理しましょう。

コールの種別 説明
非転送コール $class→hoge()Class::hoge() 等、クラス名を明示したコール
転送コール parent::, self::, static:: またはクラス階層中の forward_static_call() によるコール

冒頭の繰り返しになりますが、ドキュメントでは、 遅延静的束縛は、「直近の"非転送コール"」のクラス名を保存する と説明されています。

それってどういう状態なんだ? というのを理解するために、サンプルコードを変えます。

<?php

class Animal
{
    public static function walk(): void
    {
        self::skip(); // 転送コール
    }

    public static function skip(): void
    {
        static::run();  // static が参照するのは直近の「非転送コール」のクラス
    }

    public static function run(): void
    {
        echo '歩くことは走ることだ';
    }
}

class Human extends Animal
{
    public static function run(): void
    {
        echo '歩くな、走れ!';
    }
}

Animal::walk(); // => 歩くことは走ることだ
Human::walk(); // => 歩くな、走れ!

Human::walk() はこれ自体が非転送コールで、static:: から見ると直近の非転送コールなので、オーバーライドされた run() が実行されています。

まとめ

  • self:: のスコープは self:: を書いたクラスになり、オーバーライドできない
  • static:: を使うと遅延静的束縛により、オーバーライドが可能になる
  • static:: が参照するのは直近の非転送コールである

今まで意識していなかっただけに、仕様を整理していくのはおもしろーい!

わたし含め「とりあえず self:: にしていた」という方が、オーバーライドの必要性を考慮した上で、 static:: による遅延静的束縛もうまく利用していけるといいですね〜