PHPの「遅延静的束縛 (Late Static Bindings)」機能、解読!


PHPでは、静的メソッド(クラスメソッド)の中で3つの特別なキーワードが使えます

  • self::
  • parent::
  • static::

self::static:: を混同している人が多いと思いますが、無論別物です。

今回の焦点は「static::」です。このstatic:: を使うと「遅延静的束縛 (Late Static Bindings)」なる機能を使うことになってます。

はて… 何やら小難しそうなワードです。公式ドキュメントによれば

PHP 5.3.0 以降、PHP に遅延静的束縛と呼ばれる機能が搭載されます。 これを使用すると、静的継承のコンテキストで呼び出し元のクラスを参照できるようになります。

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

この “遅延静的束縛” という機能名は、内部動作を考慮してつけられたものです。 “遅延束縛 (Late binding)” の由来は、メソッドを定義しているクラス名を使用しても static:: の解決ができないことによります。 その代わりに、実行時情報をもとに解決するようになります。 “静的束縛 (static binding)” の由来は、 静的メソッドのコールに使用できることによります (ただし、静的メソッド以外でも使用可能です)。

だそうです。

はい!分かりませんでした!

遅延静的束縛を使わないキーワードの再確認

self::

公式ドキュメントに分かりやすい解説があったので、そのまま引用します。

self:: あるいは __CLASS__ による現在のクラスへの静的参照は、 そのメソッドが属するクラス (つまり、 そのメソッドが定義されているクラス) に解決されます。

使用例:

class A
{
    public static function print_class_name()
    {
        echo self::class . PHP_EOL;
    }
}

class B extends A
{
}

class C extends B
{
}

A::print_class_name(); // => A
B::print_class_name(); // => A
C::print_class_name(); // => A

この例から分かることは、self::が表すクラスは常にself::が記載されたクラスである。

いいね!実に解かりやすすぎです!

parent::

parent::が表すクラスは常にparent::が記載されたクラスの親クラスである。

使用例:

class A
{

}

class B extends A
{
    public static function print_parent()
    {
        echo parent::class . PHP_EOL;
    }
}

class C extends B
{
    public static function print_parent()
    {
        parent::print_parent();
    }
}

B::print_parent(); // => A
C::print_parent(); // => A

記載した時点で(実行する前から)、どのクラスを表しているのかが明白であるところはself::と同じです。

こちらも実に解りやすすぎて話にならない!

static:: キーワードを使った遅延静的束縛

まず、この「遅延静的束縛」ですが、2つの単語の組み合わせでできています。

静的束縛 (static binding) + 遅延束縛 (Late binding) = 遅延静的束縛 (Late Static Bindings)

前項では、self::parent::も、記載した時点で(実行する前から)、どのクラスを表しているのかが明白であることを話しました。

お察しの通り、static::はその逆です。

つまり、記載した時点では(実行する前からは)、どのクラスを表しているのかが明白ではないです。実行時に動的に解決されます

これで「遅延静的束縛」の「遅延」の部分の意味が分かりましたよね。

評価(static::の解決)を実行時に遅延させる」ということです。

そして、「静的」の部分の意味は公式ドキュメントの原文を再度引用して説明しますと

“静的束縛 (static binding)” の由来は、 静的メソッドのコールに使用できることによります (ただし、静的メソッド以外でも使用可能です

つまり、static::静的メソッド()というふうに使うことが多いことに因んで「静的」になったわけです。

static:: の解決ルール

static::がどのように解決されるかを話す前に、予備知識として、

  • 非転送コール(non-forwarding call)
  • 転送コール(forwarding call)

とは何かを知っておく必要があります。

非転送コール(non-forwarding call)

“転送コール” とは、self:: や parent::、static:: による静的なコール、 あるいはクラス階層の中での forward_static_call() によるコールのことです。

転送コール(forwarding call)

クラス名::の形を取った静的コール。

転送/非転送 コール の例

「転送/非転送」とは言っても、目的語がないと意味不明になりますよね。

では、そもそも「何」を転送するのか、或いはしないか?

それはね、「コール元のクラスの情報」、もっと分かりやすくいうと「呼び出し元クラスの名前」です。

転送コールは「呼び出し元クラスの名前」をコールしようとしているメソッドに伝えます。

例えば、self::foo() であれば、「呼び出し元クラスの名前は◯◯ですよ」と、foo()に教えるのです。

一方、非転送コールは違います。例えば A::foo() であれば、呼び出し元クラスの名前がAであるのは自明ですから、foo()にわざわざ教える必要はないです。だから「転送」しなくても大丈夫です。

つまり、「コール元のクラスの情報」は非転送コールによって発信され、その後、転送コールによって伝達されていくということです。

具体性に欠ける説明で分かりにくいと思うので、下記のサンプルコードで確認して下さい。

class A
{
    public static function foo()
    {
        // static:: は「呼び出し元クラス」のスコープ。
        // 「static:: は呼び出し元クラスである」と読み替えてもOK。
        echo static::class . PHP_EOL;


        // ====== `A::foo()` からきた場合 ======
        //
        // 「私を呼んだのはAだ」と自明だから、呼び出し元はクラスAである。
        // つまり、static:: は `A::` に解決される



        // ====== `parent::foo()` からきた場合 ======
        //
        // 「呼び出し元はクラスCだ」という伝言(情報)を受け取ったので、
        // static:: は `C::` に解決される



        // ====== `self::foo()` からきた場合 ======
        //
        // 「呼び出し元はクラスCだ」という伝言(情報)を受け取ったので、
        // static:: は `C::` に解決される



        // ====== `static::foo()` からきた場合 ======
        //
        // 「呼び出し元はクラスCだ」という伝言(情報)を受け取ったので、
        // static:: は `C::` に解決される
    }
}

class B extends A
{
    public static function test()
    {
        // 「非転送コール」 `C::test()` によって、このメソッドがコールされると仮定しよう
        //
        //
        // そしたら、このメソッドは次の情報を知っている
        // 「私をコールしたのはクラスCだ」 --> 「呼び出し元はクラスCだ」

        A::foo();
        // 非転送コール
        // 「呼び出し元クラスは A」 という情報ができた。
        // そして、foo() は「呼び出し元はクラスAだ」と知っている。
        // ただし、test() には関係のない情報だ。
        // test() はあくまでも「私をコールしたのはクラスCだ」以外は知る必要はない


        parent::foo();
        // 転送コール
        // 「呼び出し元はクラスCだ」ということ(情報)を foo() に伝える


        self::foo();
        // 転送コール
        // 「呼び出し元はクラスCだ」ということ(情報)を foo() に伝える


        static::foo();
        // 転送コール
        // 「呼び出し元はクラスCだ」ということ(情報)を foo() に伝える
    }
}

class C extends B
{
}


C::test();
// 非転送コール
// 「呼び出し元クラスは C」 という情報ができた。
// そして、test() は「呼び出し元はクラスAだ」と知っている。




// 実行結果
//
// A
// C
// C
// C

これで「遅延静的束縛」の正体が分かってもらえたかと思います。

ちなみに、 forward_static_call を使えば、遅延静的束縛を使った静的メソッドのコールができます。使い道はいまいち思いつかないですが…

それと、例の中のstatic::class::classという記述法はPHP 5.5以降の機能です。お使いのPHPのバージョンが古いのなら、static::class get_called_class() に置き換えて下さい。

遅延静的束縛の使い道

PHP 5.3.0以前ではselfしか使えなかったので、静的メソッドを継承した際に不都合なことがおきます

selfを使った(ダメな)継承例:

class Employee
{
    static protected $department = 'General';

    public static function getDepartment()
    {
        return self::$department . PHP_EOL;
    }
}

class WebDesigner extends Employee
{
    static protected $department = 'Web Design';
}

class Developer extends Employee
{
    static protected $department = 'Software Development';
}


echo WebDesigner::getDepartment(); // => 'General'   本当は'Web Design'であってほしい
echo Developer::getDepartment(); // => 'General'     本当は'Software Development'であってほしい

self::static::に置き換えると期待通りになります。

静的メソッドを継承した時に、static::を使えば継承先のクラスを表すことができるので、プログラミングの柔軟性が増す。使い道はここです。

以下の記事には実用例が豊富に載っているので、とても参考になります。
PHP V5.3 で遅延静的バインディングを使ったオブジェクト指向プログラミングを活用する

`$this->` と `static::` の相違点

「呼び出し元を表すキーワード」という観点から見れば、static::$this-> はよく似ています。static::は呼び出し元クラス、$this->は呼び出し元インスタンスを表します。

しかし、違う点も多いです。互換性があったりなかったりするので、注意すべし!

利用できる範囲

$this->インスタンスメソッド内でしか利用できない。

static::クラスメソッド内でも、インスタンスメソッド内でも利用できるが、静的メソッド内で利用するケースが多い。

表す呼び出し元が変化するか

$this->の場合「転送/非転送コール」の概念がないので、$thisが指す呼び出し元は最初から最後までずっと変わらない。

static::の場合、非転送コールによって、呼び出し元がころころ変わることがある。

プロパティの参照

$this->はインスタンス変数、static::はクラス変数しか参照できない。

private な プロパティ/メソッド へのアクセスの挙動

private な プロパティ/メソッド へのアクセスの挙動に関しては、static::$this->ではかなり異なるので、要注意です!

ややこしいので、サンプルコードで確認しましょう。

private な プロパティ へのアクセスの挙動

<?php

ini_set('display_errors', 1);

class A
{
    static private $private_var = 'private var in A';
    static protected $protected_var = 'protected var in A';

    protected static function bar()
    {
        echo 'static:: is class ' . static::class . PHP_EOL;
        // => static:: is class B
        //
        // static:: はクラスBを表している


        echo static::$protected_var . PHP_EOL;
        // => protected var in A
        //
        // B::$protected_var  だと考えれば当然の結果。


        echo static::$private_var . PHP_EOL;
        // => Fatal error: Cannot access private property B::$private_var
        //
        // B::$private_var にアクセスできない。
        // B::$private_var はちゃんと定義されているのに、アクセスできない!?
        // 理由は、実行時のコンテキストが `B` ではなく、`A` だからだ。
        // 言ってみれば `B` の外(`A`)からは `B` の private な静的プロパティにアクセスできないということだ。
        //
        // ところが、インスタンスメソッドの中で `$this->` を使ってprivate なメンバーをアクセスする場合、
        // 逆に実行時のコンテキスト内の private なメンバーを優先してアクセスしようとします。
        // それに対して、クラスメソッドの場合は、定義したクラス内の private なメンバーをアクセスしようとする。だからエラーが起きる。
        //
        // クラスメソッドの挙動のほうが自然に思えますが、挙動が統一してなくて、気をつけないとすぐ混乱しそうです...
        //
        // つまり、private な静的プロパティは、そのプロパティを定義したクラスのコンテキストからでないと、絶対にアクセスできない。
        // ゆえに、private な静的プロパティへのアクセスは常に self:: を使うべし。これ、大事。
    }
}

class B extends A
{
    static private $private_var = 'private var in B';

    public static function foo()
    {
        static::bar();
    }
}


B::foo();


// 実行結果
//
// static:: is class B
// protected var in A
// Fatal error: Cannot access private property B::$private_var

サンプルコードを読んでもピンとこない人はたぶん$this->privateなメソッド()の挙動に対する理解が足りないと思われるので、先にこちらの記事をご覧下さい。

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

private な プロパティ へのアクセスの挙動

一見落着のように見えましたが、なんと!「private な プロパティ」となると、また挙動が変わるやで…! なぜこんな仕様になったんだろう…

ini_set('display_errors', 1);

class A
{
    protected static function bar()
    {
        static::baz();
        // => 'baz() in A'
        //
        // `B::baz()` に相当する
        // B には baz() が定義されていないが、A の baz() を継承した。
        // 継承したけど、A の コンテキスト からしかコールできない。
        // ここは A の コンテキストだから、問題なくコールできた。


        static::foobar();
        // => Fatal error: Call to private method B::foobar() from context 'A'
        //
        // `B::foobar()` に相当する
        // B には foobar が定義されていて、そして、B の コンテキストからしかアクセスできない。
        // ここは A の コンテキストだから、アクセスできないため、エラーがおきる。
    }

    private static function baz()
    {
        echo "baz() in A" . PHP_EOL;
    }

    private static function foobar()
    {
        echo "foobar() in A" . PHP_EOL;
    }
}

class B extends A
{

    public static function foo()
    {
        static::bar();
    }


    // private static function baz();
    //
    // A::baz() が B にコピーされるが、
    // A の コンテキスト からしかアクセスできない
    // つまり、B の中で B::baz() はダメだが、
    // A の 中で B::baz() はOK。


    private static function foobar()
    {
        // A::foobar() を上書きしたので、新しいメソッドのスコープは B となる
    }
}


B::foo();


// 実行結果
//
// baz() in A
// Fatal error: Call to private method B::foobar() from context 'A'

公式ドキュメントでは、このように説明しています。

静的でないコンテキストでは、呼び出されるクラスはそのオブジェクトのクラスと同じになります。 $this-> は private メソッドを同じスコープからコールしようとするので、 static:: を使うと異なる結果となります。もうひとつの相違点は、 static:: は静的なプロパティしか参照できないということです。

$this->privateメソッド()は、常に実行時のコンテキスト内のprivateなメソッドを参照するのに対して、static::privateメソッド()privateメソッド()を定義したクラスのコンテキストからしかアクセスできない(publicprotectedなメソッドならアクセスできる)。

いやはや… この仕様はどうなのよ(笑) PHP 8に乞うご期待って感じです。

余談、`->` と `::` には互換性がある?

今回いろいろリサーチしてたら始めて知ったのですが、どうやら 矢印演算子 ->スコープ定義演算子 (::) は互換可能らしい。

ただし、「クラスの中」と「メソッドの参照」とう条件付きがあります。

ご覧あれー

ini_set('display_errors', 1);

class A
{
    public $baz = 'baz';
    public static $qux = 'qux';

    public static function foobar()
    {
        echo 'foobar' . PHP_EOL;
    }

    public function foo()
    {
        echo 'foo' . PHP_EOL;
        return $this;
    }

    public function bar()
    {
        $this::foo()->foo()::foo();
        $this::foobar();

        $this->foo()::foo();
        $this->foobar();
    }
}

$a = new A();
$a->bar();


// 実行結果
//
// foo
// foo
// foo
// foobar
// foo
// foo
// foobar

な〜んじゃあこりゃー!?

なんというカオス…!! これ以上はおやめになって…

まとめ

  • 「遅延静的束縛」機能の核は、staticを「遅延評価」するところにある。
  • staticは「呼び出し元クラス」に解決される。
  • 呼び出し元クラスが変わると、staticが表すクラスも違ってくることに注意を払うべき。
  • $this->static:: は似ているが、互換性もなければ、相違点も多い。
  • 「遅延静的束縛」の主な使い道は「クラスメソッドを継承した際の動的な解決(= 柔軟性が高い)」。

参考記事


コメントを残す

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

ABOUTこの記事をかいた人

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