NULL安全とは
NULLによるエラーをコンパイルまたは静的解析で検出し、実行時にNULLによるエラーを起こさないことです。NULL安全が高いか低いかは基本的にはプログラム言語に依存します。型には基本的にNULL許容型とNULL非許容型があり、それらが別物として扱われることがNULL安全を満たす条件です。理由は後述します。
PHPにおけると銘打っておきながら、まずはkotlinという言語を例に上げてNULL安全の説明をします。kotlinは静的型付き言語なので基本的に型を定義していくのですが、通常定義される型はNULL非許容型になります。NULL非許容型とはその名の通りNULLを代入できない型のことです。
//NULL非許容型
var intObject: Int = 10
var stringObject: String = "test"
var sampleObject: SampleObject = SampleObject()
//以下をコンパイルするとエラー(最近はIDEで怒ってくれる)
var intObject: Int = null
var stringObject: String = null
var sampleObject: SampleObject = null
NULL許容型(NULLも代入できる型)を利用するには明示する必要があります。
//NULL許容型
var intObject: Int? = 10
var stringObject: String? = "test"
var sampleObject: SampleObject? = SampleObject()
//以下はコンパイルしてもエラー出さない
var intObject: Int? = null
var stringObject: String? = null
var sampleObject: SampleObject? = null
NULL許容型を参照する場合は以下の制約が設けられます。変数がNULLであっても何らかの意図的な処理を行わせるというニュアンスです。
//セーフコール演算子(変数がNULLならその先は実行せずNULLを返す)
var stringObject: String? = "test"
var stringObjectLength: Int? = stringObject?.length
//非NULLアサーション演算子(変数がNULLならNullPointerExceptionを投げる)
var intObject: Int? = 10
var addObject: Int = 10 + intObject!!
//if文でNULLじゃないことを保証する
var intObject: Int? = 10
if (intObject != null) {
var addObject: Int = 10 + intObject
}
NULL許容型とNULL非許容型を別物として扱うことで、コンパイル時にNULL許容型が参照ルールを守っているかをチェックできるので、コンパイル時点でNULLによるエラーをすべて洗い出せるよねというのがNULL安全の思想です。
NULL安全のメリットとしてはコーディング中に変数が扱いやすくなることだと思います。ある変数を利用するとき、その変数の宣言時の型がNULL非許容型であればその型が(lateinitとかはありますが)確実に入っているので、その変数がどういった処理を辿ってきたかやNULLが入る可能性を考慮してif文で防御するといった作業が不要になります。NULL許容型の場合だけ、その変数の処理を確認して利用時点での正しい参照方法を選択すればよいです。強い制約下の元コーディングが行えるので認知不可が軽減でき、その制約も明示すれば回避できるので縛られすぎることもないです。
PHPにおけるNULL安全
話が長くなりましたが本題のPHPにおけるNULL安全に移りたいと思います。前提としてPHPはNULL安全が低いです。NULL許容型とNULL非許容型を区別してコンパイラまたは静的解析時にNULL許容型に対してチェックを行うことでNULL安全は高まるので、実行時型が決定される動的型付き言語だとそもそも難しいです。
ただ書き方次第でNULL安全は高めることが出来ます。
①引数、戻り値の型を指定する。
PHPの引数、戻り値に設定される型はNULL非許容型です。なので型を指定しておけば実行しなくてもintelephenseやStanでエラーを出してくれます。
※型の制約はあくまで引数として受け取った時だけで、ローカル変数として使う場合は型の制約がないので注意。
function foo(string $name) {
return $name;
}
foo(null); //TypeError
function too(): string {
return null;
}
too() //TypeError
NULL許容型の場合は以下のように書きます。
function doo(?string $name): ?string {
return $name;
}
doo(null) //エラーなし
②クラス変数宣言時に型を指定する。
クラス変数に設定される型もNULL非許容型です。これも型を指定しておけば実行しなくてもintelephenseやStanでエラーを出してくれます。PHPStanはLevel8からNULL非許容型プロパティのアクセスをチェックします。
//NULLの参照。PHPStan解析レベル8から怒ってくれる。「$this->testValue ?? ""」にしたら怒られない。
class Test {
public ?string $testValue;
function doo(): void {
echo(mb_strlen($this->testValue));
}
function too(): void {
$this->testValue = "ddd";
}
}
//NULLの代入。PHPStan解析レベル8で怒ってくれる
class Test {
public string $testValue;
function doo(): void {
$this->testValue = null;
}
}
//以下のようなケースはPHPStan解析レベル8でも怒ってくれない。tooの戻り値が「null」とか「null|string」とかなら怒ってくれるので引数、戻り値の型指定が重要になる
class Test {
public string $testValue;
function doo(): void {
$this->testValue = $this->too();
}
function too() {
return null;
}
}
まとめると、引数、戻り値の型を指定して、クラス変数の型を指定して、PHPStanの解析レベルを8まで上げたら比較的NULL安全になります。道のりは長いですが頑張りましょう。