2018-04-15

遅延静的束縛について理解しよう

PHPで「static」というキーワードの使われ方は複数あります。
今日は、スコープ定義演算子(ダブルコロン::)とともに用いるstatic、つまり
遅延静的束縛の意味でのstaticについて見てみましょう。

この記事では、次のようにパートを分けています。
§1.PHPでのstaticキーワード
§2.スコープを表す方法

§1.PHPでのstaticキーワード

PHPにおいて、staticというキーワードの使い方には次の場合があります。
1.静的なメソッド・プロパティを定義するためのstatic
2.静的変数を定義するためのstatic
3.メソッドのスコープを定義するための遅延静的束縛の意味でのstatic

今日は3の遅延静的束縛の意味でのstaticを理解することが目的ですが、1と2についても触れておきましょう。

1-1.静的なメソッド・プロパティを定義するためのstatic

<?php
class Foo {
    public $aProperty = 'foo';    //静的でないプロパティの宣言
    public static $aStaticProperty = 'bar';    //静的なプロパティの宣言
    public function aMethod() {    //静的でないメソッドの宣言
        echo 'baz';
    }
    public static function aStaticMethod() {    //静的なメソッドの宣言
        echo 'qux';
    }
}
// 静的でないプロパティを動的にコールする
$sample = new Foo;
echo $sample->aProperty;  // foo

// 静的でないプロパティを静的にコールする
echo Foo::aProperty;  // エラー

// 静的プロパティを静的にコールする
echo Foo::$aStaticProperty;  // bar
 
// 静的メソッドを静的にコールする
Foo::aStaticMethod();  // qux
?> 

staticメソッドはオブジェクトのインスタンスを生成せずに コールすることができます。これを静的コールと言います。
インスタンス化されたクラスオブジェクトからメソッドやプロパティにアクセスすることを動的コールと言います。
静的なプロパティを動的コールすることは出来ませんが、静的なメソッドを動的コールすることは可能です。
これを整理してみましょう。
  動的コール   静的コール
静的でないプロパティー エラー
静的なプロパティー エラー
静的でないメソッド PHP5ではE_STRICT レベルの警告
PHP7では非推奨、E_DEPRECATED レベルの警告
将来的にサポートされなくなる見込み
静的なメソッド

1-2.静的変数を定義するためのstatic

静的変数のスコープは、自信が定義されたローカル関数内のみですが、プログラム実行がこのスコープの外で行われるようになってもその値を保持します。
<?php
function test()
{
    static $a = 0;
    echo $a.PHP_EOL;
    $a++;
}

test();  //0
test();  //1
test();  //2
?>

1-3.遅延静的束縛の意味でのstatic

遅延静的束縛の意味でのstaticについては、§2で詳述します。
PHPのマニュアルのサンプルコードのコメントには次のように書かれています。
"PHP5.3.0から'static'をスコープの値として使用できます。selfと比較してより柔軟に継承の仕組みを使えるようになります。"
このコメントは、遅延静的束縛のstaticの役割を理解する大きなヒントになります。
<?php 
// As of php 5.3.0, you can use 'static' as scope value as in below example 
// (add flexibility to inheritance mechanism compared to 'self' keyword...) 
class A { 
    const C = 'constA'; 
    public function m() { 
        echo static::C; //遅延静的束縛の意味でのstatic
    } 
} 

class B extends A { 
    const C = 'constB'; 
} 

$b = new B(); 
$b->m(); 

// output: constB 
?>


§2.スコープを表す方法


PHPでのstaticキーワードの使用方法について見て来ましたが、
次は遅延静的束縛の意味でのstaticとともに用いられるスコープ定義演算子「::」について見てみましょう。 スコープ定義演算子を使用してスコープを表すには、下記の方法があります。
記載方法 スコープ 記載できる場所
1 クラス名:: 明示されたクラス クラス定義の外or中
2 self:: self::が記載されたクラス クラス定義の中
3 parent:: parent::が記載されたクラスの親クラス クラス定義の中
4 static:: 直近の "非転送コール" のクラス クラス定義の中


2-1. クラス名::

明示されたクラスのメソッド(またはプロパティー)をコールします。
§1で見た、メソッドやプロパティーを静的にコールする方法でもあります。次のサンプルでは、メソッドをクラス定義の外からコールしています。
<?php
class Foo {
    public function myMethod() {
        echo 'baz';

    }
} 
// メソッドを静的にコールする
Foo::myMethod();  // baz

// メソッドを静的にコールする PHP 5.3.0 以降で対応
$classname = 'Foo';  // baz
$classname::myMethod();  // baz
?>
あまり意味はありませんが、クラス内からもクラス名を明示してコールしてみましょう。
<?php
class Foo {
    public static function methodA() {
        Foo::methodB();  // 自クラス内から自クラスを明示してメソッドをメソッドをコールする
    }
    
    public static function methodB() {
        echo 'baz';
    }
} 

Foo::methodA();  // baz
?>
上のサンプルのクラス名のFoo::の部分をself::と書き換えても同じ結果になります。

2-2. self::

self::が記載されたクラスのメソッド(またはプロパティー)をコールします。
<?php
class Foo {
    public static function methodA() {
        self::methodB();  // 自クラス内から自クラスを明示してメソッドをメソッドをコールする
    }
    
    public static function methodB() {
        echo 'baz';
    }
} 

Foo::methodA();  // baz
?>

2-3. parent::

parent::が記載されたクラスの親クラスのメソッド(またはプロパティー)をコールします。
次のサンプルは、子クラスで親クラスのメソッドをオーバーライドしていても、親クラスのメソッドをコールできるという例です。
<?php
class MyClass
{
    protected function myFunc() {
        echo "foo";
    }
}

class OtherClass extends MyClass
{
    // 親の定義をオーバーライドします
    public function myFunc()
    {
        // それでも親の関数をコールできます
        parent::myFunc();
    }
}

$class = new OtherClass();
$class->myFunc();  // foo
?>

2-4. static::

やっと本題に辿り着きました。
遅延静的束縛(Late Static Bindings)とは、「非転送コール」によって呼ばれたメソッド・クラス名を保持する機能です。
static::では、遅延静的束縛によって保持するクラスのメソッド(またはプロパティー)をコールします。
<?php
class A {
    public static function who() {
        echo __CLASS__;
    }
    public static function test() {
        static::who(); // これで、遅延静的束縛が行われます
    }
}

class B extends A {
    public static function who() {
        echo __CLASS__;
    }
}

B::test();  // B
?>

「転送コール」と「非転送コール」とは

非転送コール: Foo::test()や$foo->test()といったクラス名(もしくはオブジェクト)を明示した呼び出し
転送コール: メソッド内でself::、parent::、static::を使用して行われる呼び出し

遅延静的束縛が導入された背景

self::は、「self::」が記載されたクラスのメソッド(またはプロパティー)しかコールすることが出来ません。
遅延静的束縛で実行時に最初にコールされたクラスを参照することにより、この制限を取り払っています。
下記はself::による制限の例です。
<?php
class A {
    public static function who() {
        echo __CLASS__;
    }
    public static function test() {
        self::who();  // クラスBを明示して呼び出しても、クラスAのwho()がコールされる
    }
}

class B extends A {
    public static function who() {
        echo __CLASS__;
    }
}

B::test();  // A
?>
「2-4. static::」のサンプルコードは、上記のコードと7行目が異なるだけですが、static::を使用することでクラスBのwho()のコールを可能にしています。


参考リンク

PHPマニュアル staticキーワード
PHPマニュアル スコープ定義演算子 (::)
PHPマニュアル 遅延静的束縛 (Late Static Bindings)
maeharinの日記 PHPを愛する試み 〜self:: parent:: static:: および遅延静的束縛〜
明滅するプログラマの思索 クラス内 static プロパティについてまとめ