function foo(int $x = null)
よく見たら不自然なんだけどこれまで普通に許されていた、この引数デフォルト値がついに禁止されます。
何がおかしいって型がint
なのに引数を渡さないと$xがnullになるので矛盾してしまうわけですね。
これはPHPが昔からの仕様を残しておいたためであり、いわゆる歴史的経緯というやつです。
以下は該当のRFC、Deprecate implicitly nullable parameter typesの日本語訳です。
PHP RFC: Deprecate implicitly nullable parameter types
Introduction
PHP7.1で?T構文、さらにPHP8.0でUnion型がサポートされたことにより、PHPはnull許容値型を正しく書くことができます。
歴史的にはPHP5.0でオブジェクト型、PHP5.1でarray、PHP5.4でcallable、PHP7.0でスカラー型の引数が使えるようになりました。
しかし、当時はnull許容値型の構文がなかったため、デフォルト値としてnullを許容することとし、PHPの引数は暗黙的にnull許容値型になりました。
暗黙的なnull許容値型は他の言語規則と矛盾しており、実際にどのような型を受け取れるかがわかりにくくなってしまいます。
また、現在は以下のようなメソッド定義も可能です。
function foo(T1 $a, T2 $b = null, T3 $c) {}
必須引数の前にオプション引数が定義されているように見えます。
この順番でのシグネチャはPHP8.0で非推奨になりましたが、暗黙的なnull許容値型については互換性のため現状維持とされました。
この判断に伴って、非推奨通知に微妙なバグも発生しました。
function bar(T1 $a, ?T2 $b = null, T3 $c) {}
こちらはPHP8.1以上でE_DEPRECAETEDが発生します。
function test(T1 $a, T2|null $b = null, T3 $c) {}
しかしこちらはPHP8.3以上でないと発生しません。
暗黙的なnull許容値型のもうひとつの問題は、継承に関してです。
子クラスが親クラスとまったく同じシグネチャを持っているはずなのに、デフォルト値が異なっているとLSP違反になることがあります。
暗黙的なnull許容値型のサポートはユーザランドに混乱をもたらすだけではなく、PHPエンジン側もエッジケースを処理する必要があり、不必要な複雑さやバグの原因にもなります。
null許容値型を書くことができなかった制限はもうPHPには存在しないので、暗黙的なnull許容値型を非推奨にすることを提案します。
Proposal
暗黙的なnull許容値型を非推奨にします。
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個が暗黙的なnull許容値型を使っていました。
これほど多い理由のひとつは、かつてSymfonyのコーディングスタイルが暗黙的なnull許容値型を推奨していたからです。
しかしその後Symfonyはコーディングスタイルをnull許容値型に変更しました。
また、T $parameter = null
から?T $parameter = null
に自動整形するツールもたくさん存在します。
たとえばPHP-CS-Fixerのnullable_type_declaration_for_default_null_value、PHP_CodeSnifferのSlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValueです。
この変更で起きる可能性のある問題点は、オプション引数が必須引数の前に来る可能性があるということです。
前述のように、この順番はPHP8.0で非推奨になったため、新たな非推奨通知が発生する可能性があります。
これは7年も前から存在する?T
構文で対応可能であり、さらに自動的に変換するツールも存在します。
この修正はシグネチャの変更だけで対応可能であり、他に波及することもないため、対応は容易です。
Code change examples
コードの例。
class Foo {
public function bar(int $x = null, float $y) {
// ...
}
}
上のメソッドは、以下のように変更します。
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を渡すことは元からできません。
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を書いていきましょう。
後者は今でも割とやってるけど。