【PHP8.4】メソッド引数のデフォルトnullがついに禁止される

```php
function foo(int $x = null)
```

よく見たら不自然なんだけどこれまで普通に許されていた、この引数デフォルト値がついに禁止されます。

何がおかしいって型が`int`なのに引数を渡さないと$xがnullになるので矛盾してしまうわけですね。
これはPHPが昔からの仕様を残しておいたためであり、いわゆる歴史的経緯というやつです。

以下は該当のRFC、[Deprecate implicitly nullable parameter types](https://wiki.php.net/rfc/deprecate-implicitly-nullable-types)の日本語訳です。


# PHP RFC: Deprecate implicitly nullable parameter types

## Introduction

PHP7.1で[?T構文](https://wiki.php.net/rfc/nullable_types)、さらにPHP8.0で[Union型](https://wiki.php.net/rfc/union_types_v2)がサポートされたことにより、PHPはnull許容値型を正しく書くことができます。

歴史的にはPHP5.0でオブジェクト型、PHP5.1でarray、PHP5.4でcallable、PHP7.0でスカラー型の引数が使えるようになりました。
しかし、当時はnull許容値型の構文がなかったため、デフォルト値としてnullを許容することとし、PHPの引数は暗黙的にnull許容値型になりました。

暗黙的なnull許容値型は他の言語規則と矛盾しており、実際にどのような型を受け取れるかがわかりにくくなってしまいます。
また、現在は以下のようなメソッド定義も可能です。

```php
function foo(T1 $a, T2 $b = null, T3 $c) {}
```

必須引数の前にオプション引数が定義されているように見えます。
この順番でのシグネチャはPHP8.0で非推奨になりましたが、暗黙的なnull許容値型については[互換性のため現状維持](https://github.com/php/php-src/pull/5067)とされました。
この判断に伴って、非推奨通知に微妙なバグも発生しました。

```php
function bar(T1 $a, ?T2 $b = null, T3 $c) {}
```

こちらは[PHP8.1](https://github.com/php/php-src/commit/c939bd2f10b41bced49eb5bf12d48c3cf64f984a)以上でE_DEPRECAETEDが発生します。

```php
function test(T1 $a, T2|null $b = null, T3 $c) {}
```

しかしこちらは[PHP8.3](https://github.com/php/php-src/pull/11497)以上でないと発生しません。


暗黙的なnull許容値型のもうひとつの問題は、継承に関してです。
子クラスが親クラスとまったく同じシグネチャを持っているはずなのに、デフォルト値が異なっているとLSP違反になることがあります。

暗黙的なnull許容値型のサポートはユーザランドに混乱をもたらすだけではなく、PHPエンジン側もエッジケースを処理する必要があり、不必要な複雑さやバグの原因にもなります。

null許容値型を書くことができなかった制限はもうPHPには存在しないので、暗黙的なnull許容値型を非推奨にすることを提案します。


## Proposal

暗黙的なnull許容値型を非推奨にします。

```php
function foo(T $var = null) {}

// Deprecated: Implicitly marking parameter $var as nullable is deprecated, the explicit nullable type must be used instead
```

またPHP9で、暗黙的なnull許容値型を禁止します。


## Backward Incompatible Changes

暗黙的なnull許容値型を使うと、非推奨のE_DEPRECATEDが発行されます。


### Impact analysis and migration paths

Composerの上位2000パッケージのうち、[880個](https://gist.github.com/kocsismate/cf3bdfbf35eb10224ee5ecd29b39656b)が暗黙的なnull許容値型を使っていました。
これほど多い理由のひとつは、かつてSymfonyのコーディングスタイルが暗黙的なnull許容値型を推奨していたからです。

しかしその後Symfonyは[コーディングスタイルをnull許容値型に変更](https://github.com/symfony/symfony/pull/53612)しました。

また、`T $parameter = null`から`?T $parameter = null`に自動整形するツールもたくさん存在します。
たとえば[PHP-CS-Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer)のnullable_type_declaration_for_default_null_value、[PHP_CodeSniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer/)のSlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValueです。

この変更で起きる可能性のある問題点は、オプション引数が必須引数の前に来る可能性があるということです。
前述のように、この順番は[PHP8.0で非推奨になった](https://github.com/php/php-src/pull/5067)ため、新たな非推奨通知が発生する可能性があります。
これは7年も前から存在する`?T`構文で対応可能であり、さらに自動的に変換するツールも存在します。
この修正はシグネチャの変更だけで対応可能であり、他に波及することもないため、対応は容易です。


## Code change examples

コードの例。

```php
class Foo {
    public function bar(int $x = null, float $y) {
        // ...
    }
}
```

上のメソッドは、以下のように変更します。

```php
class Foo {
    public function bar(?int $x, float $y) {
        // ...
    }
}
```


## Version

PHP8.4。


## Vote

投票期間は2024/02/28から2024/03/13、投票者の2/3の賛成で受理されます。

本RFCは賛成26反対0の全員賛成で受理されました。

PHP8.4からE_DEPRECATEDとなり、PHP9で例外になります。


# 感想

いやまあ、おかしいなとは思いつつも普通に使っていたので、これが使えなくなると修正が必要なところがたくさん出てきてしまいますね。
実際やることは`Code change examples`で示されているように`int $x = null`を`?int $x`にするだけであり、フォーマッタが自動でやってくれるレベルです。
自分で書いている部分であれば自動変換ツールでどうにでもなるのですが、問題はライブラリです。
2000のうち880も存在するライブラリのどれほどが、この変更についてきてくれるかは怪しい気がしますね。
プルリクを出して回るくらいの対策は取っておいたほうがいいのではないでしょうか。

なお、デフォルト値ではなく引数としてnullを渡すことは元からできません。

```php
function hoge(int $a){}

hoge(null); // Uncaught TypeError: hoge(): Argument #1 ($a) must be of type int,  null given
```

ということで引数とデフォルト値の整合性が取れるようになりました。

PHPは7.0あたりから型まわりの強化がどんどん進められていて、今となってはもはや、きちんと書きさえすれば既に相当厳格な型運用が可能になっています。
いつのまにかintがstringになっていたり、なんでもかんでもarrayに突っ込んでどうにかしたりといった古き良きPHPに別れを告げ、これからはかっちりしたPHPを書いていきましょう。

後者は今でも割とやってるけど。