【PHP8.3】static変数に動的値を突っ込めるようになる

PHPには[static変数](https://www.php.net/manual/ja/language.variables.scope.php#language.variables.scope.static)という仕組みが存在します。

```php
function foo(){
    static $i = 1;
    echo $i++;
}

foo(); // 1
foo(); // 2
foo(); // 3
```

`static $i=1;`は最初に呼ばれたときに一回だけ初期化され、その後は関数を抜けても値が保持され続けます。
メモ化や再帰処理などに便利な機能です。

ただし、static変数の宣言には定数式しか使えず、関数や変数などは使えません。

```php
function foo($param){
    static $i = $param; // Fatal error: Constant expression contains invalid operations
    static $j = bar();  // Fatal error: Constant expression contains invalid operations
}
```

これは別に技術的理由ではなく、昔からそうだったからというだけだったみたいで、ここに関数値とかも使えるようにしようというRFCが提出されました。
既に受理されており、PHP8.3から動的な値が使えるようになります。
以下は該当のRFC、[Arbitrary static variable initializers](https://wiki.php.net/rfc/arbitrary_static_variable_initializers)の紹介です。


# PHP RFC: Arbitrary static variable initializers

## Proposal

PHPでは、関数の内部でstatic変数を宣言できます。
static変数は関数よりも長生きで、複数回呼び出しても値が継続されます。

```php
function foo() {
    static $i = 1;
    echo $i++, "\n";
}

foo(); // 1
foo(); // 2
foo(); // 3
```

ただし、`static $i = 1;`の構文において、右辺は定数式でなければなりません。
すなわち、関数を呼び出したり、引数を使ったりすることはできないということです。
ユーザの立場としては、この制限は理解しがたいものです。

このRFCでは、制限を緩め、static変数のイニシャライザに任意の式を記述できるようにします。


```php:PHP8.3
function bar() {
    echo "bar() called\n";
    return 1;
}
 
function foo() {
    static $i = bar();
    echo $i++, "\n";
}
 
foo(); // bar() called  1
foo(); // 2
foo(); // 3
```

## Backwards incompatible changes

後方互換性のない変更点。

### Redeclaring static variables

現在、static変数の再宣言は許可されているのですが、この意義は甚だ疑問です。

```php:PHP8.2
function foo() {
    static $x = 1;
    var_dump($x); // 2
    static $x = 2;
    var_dump($x); // 2
}

foo();
```

static変数はコンパイル時に解釈されるため、この値は後者に統一されます。
これは直感的な動作ではなく、有用でもありません。

本RFCではstatic変数の再定義を禁止とし、コンパイルエラーを発生させることにします。

```php:PHP8.3
function foo() {
    static $x = 1;
    static $x = 2;
}
// Fatal error: Duplicate declaration of static variable $x
```

### ReflectionFunction::getStaticVariables()

[ReflectionFunction::getStaticVariables()](https://www.php.net/manual/ja/reflectionfunctionabstract.getstaticvariables.php)は、static変数と現在の値を取得できます。
現在この関数は、static変数が一度も定義されていなかった場合には、定数式を評価してstatic変数を初期化します。
本RFCにおいて、static変数が動的になったことにより、この動作はできなくなります。

今後は、まずコンパイル時に定数式の評価を試み、成功すればその値をstatic変数テーブルに登録しますが、できなかった場合はNULLで初期化します。
関数が実行されてstatic変数が定義されると、その値で変数テーブルが更新され、後は`ReflectionFunction::getStaticVariables()`で値を取れるようになります。

```php
function foo($initialValue) {
    static $x = $initialValue;
}

var_dump((new ReflectionFunction('foo'))->getStaticVariables()['x']); // NULL

foo(1);
var_dump((new ReflectionFunction('foo'))->getStaticVariables()['x']); // 1

foo(2);
var_dump((new ReflectionFunction('foo'))->getStaticVariables()['x']); // 1
```

この例を見れば、関数実行前にstatic変数値を評価できない理由は明らかでしょう。


## Other semantics

その他

### Exceptions during initialization

イニシャライザは例外を投げるかもしれません。
その場合static変数は未初期化のままであり、次の実行時に再度イニシャライザが呼び出されます。

```php
function bar($throw) {
    echo "bar() called\n";
    if ($throw) throw new Exception();
    return 42;
}

function foo($throw) {
    static $x = bar($throw);
}

try {
    foo(true); // bar() called
} catch (Exception) {}

var_dump((new ReflectionFunction('foo'))->getStaticVariables()['x']); // NULL

foo(false); // bar() called
var_dump((new ReflectionFunction('foo'))->getStaticVariables()['x']); // int(42)

foo(true); // bar()は呼ばれない
```

### Destructor

デストラクタで例外をスローするローカル変数をstatic変数で上書きした場合、static変数への値の代入は例外の発生前に行われます。

```php
class Foo {
    public function __destruct() {
        throw new Exception();
    }
}
 
function foo($y) {
    $x = new Foo();
    static $x = $y;
}
 
try {
    foo(42);
} catch (Exception) {}
 
var_dump((new ReflectionFunction('foo'))->getStaticVariables()['x']); // 42
```


### Recursion

static変数のイニシャライザは、static変数が初期化されていない場合にのみ実行されます。
ところでイニシャライザが自身を再帰で呼び出す場合、呼び出し先でstatic変数はまだ初期化されていません。
即ち、この場合イニシャライザは複数回実行されます。
ただしstatic変数への代入は一回だけしか行われません。
再帰的なstatic変数イニシャライザの有用なシナリオはありそうにもないので、このセマンティクスは特に重要ではないでしょう。

```php
function foo($i) {
    static $x = $i < 3 ? foo($i + 1) : 'Done';
    var_dump($x);
    return $i;
}

foo(1);
// string(4) "Done"  $i = 3
// string(4) "Done"  $i = 2
// string(4) "Done"  $i = 1
 
foo(5);
// string(4) "Done"  $i = 5  イニシャライザは呼ばれない
```

### What initializers are known at compile-time?

議論の中で、他のstatic変数に依存するstatic変数はコンパイル時に既知であるだろうかという疑問が発生しました。

```php
function foo() {
   static $a = 0;
   static $b = $a + 1;
}
```

答えはノーです。
この例においては、`$b`の初期化まで`$a`の値は0であることは確かです。
しかし、必ずしもそうとは限りません。
もし`$a`がどこかで変更された場合、`$b`の初期値も変わってしまいます。


## Vote

投票者の2/3の賛成で受理されます。

投票期間は2023/03/21から2023/04/04です。
本RFCは、賛成25反対0の全会一致で可決されました。



# 感想

ここに動的値を使いたいことってある？
再帰処理で再帰回数を指定したいときとかかな。
私は再帰処理を滅多に書かないので需要がどの程度あるかはわからないのですが。

しかし満場一致で採択されたということは、おそらくそれだけ需要があるということでしょう。
みなさんは、どんなときにこの機能を使いますか？