かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

PHP オプショナルな引数の初期値を設定したい

最近 WordPress を触る機会が復活してきて PHP の書き方忘れまくってるので TIL 的にメモを残しておこうと思いました。

オプショナルな仮引数を持つ関数内で変数の初期値を設定したい

<?php
function myFunc($x = null) {
  // $foo に初期値を設定したい
  $foo = $x;
  var_export($foo);
}
環境

🙅 ?: 三項演算子の省略形は falsy な値も弾かれてしまう

$x = $a ?: $b$x = $a ? $a : $b と同等。
この時 $a ?!empty($a) ? と同等なので falsy な値が引数として渡された時にデフォルト値が使われてしまう

<?php
function myFunc($x = null) {
  $foo = $x ?: 'default';
  var_export($foo);
}

myFunc(); // => 'default'
myFunc(1); // => 1
myFunc(null); // => 'default'
myFunc(false); // => 'default'
myFunc(0); // => 'default'
myFunc(''); // => 'default'
myFunc([]); // => 'default'

Falsy な値の時もデフォルト値を使いたい場合は良いと思うけど、その挙動にしていることを覚えてないとバグの温床になりそうな気がする。

🙆 ?? null合体演算子を使うのが良い (PHP v7.x 以上)

$x = $a ?? $bif (isset($a)) { $x = $a } else { $x = $b } と同等。

<?php
function myFunc($x = null) {
  $foo = $x ?? 'default';
  var_export($foo);
}

myFunc(); // => 'default'
myFunc(1); // => 1
myFunc(null); // => 'default'
myFunc(false); // => false
myFunc(0); // => 0
myFunc(''); // => ''
myFunc([]); // => array()

isset() なので引数に null を意図的に渡した場合はデフォルト値が返される

cf. PHP 7 変数に値がなければデフォルト値を設定したい。 - かもメモ

PHP v7.4 以上では ??= null合体代入演算子 が使える

$a ??= $b$anull なら $a$b を代入する。

<?php
$a ??= $b;
// 下記と同等
$a = $a ?? $b;

null合体代入演算子 ??= を使うと下記の用に書き換えられる

<?function myFunc($x = null) {
  $x ??= 'default';
  var_export($x);
}

myFunc(); // => 'default'
myFunc(1); // => 1
myFunc(null); // => 'default'
myFunc(false); // => false
myFunc(0); // => 0
myFunc(''); // => ''
myFunc([]); // => array()

個人的に null合体代入演算子 ??= は元の引数の状態を変えてしまうのであまり好みではないかも。


引数を型で縛る

そもそも引数に何でも渡せてしまうのがバグ発生の元。
PHP の型宣言を使って入力値を絞ってしまえば関数内での考慮することがシンプルになります

PHP5から「タイプヒンティング(型宣言)」は存在したが、そこからずっと中途半端な状態が続きようやくPHP7で機能拡張されたことによってスカラ型の宣言も行えるようになった。 また、PHP7.2からobject型も扱えるようになっている。
cf. [PHP] 型宣言(PHP7) - Qiita

null を許容する型宣言

nullable (PHP v7.1 以降)
型名の前に ? を付けると nullable な型になる

<?php
function myFunc(?int $x): void {
  $foo = $x ?? 'default';
  var_export($foo);
}

myFunc(10); // => 10
myFunc(null); // => 'default'
myFunc("");
// => Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int
myFunc();
// => Fatal error: Uncaught ArgumentCountError: Too few arguments to function myFunc(), 0 passed

nullable な型は null を渡すことができるがデフォルト引数を設定してないとオプショナルな引数という感じではない
そもそも null を明示的に渡したい理由がある場合に使ったほうが良さそう

デフォルト引数だけで null を許容する

仮引数の型宣言があってもデフォルト引数で null を設定することができる
これを利用すると型付のオプショナルな引数を作れる

<?php
function myFunc(int $x = null): void {
  $foo = $x ?? 'default';
  var_export($foo);
}

myFunc(); // => 'default'
myFunc(10); // => 10
myFunc([]);
// => Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int,

PHP の型宣言は型を厳密にチェックしてるのではない!?

先の例を試していて int: $x と型宣言をしているにも関わらず 1null, true, false などは型エラーにならず関数が実行されてしまいました。

<?php
function myFunc(int $x = null): void {
  $foo = $x ?? 'default';
  var_export($foo);
}

myFunc(); // => 'default'
myFunc(10); // => 10
// 型が異なるが実行されてしまう
myFunc('1'); // => 1
myFunc(null); // => 'default'
myFunc(true); // => 1
myFunc(false); // => 0

// float は warning が出るが int に変換されて実行される
myFunc(1.2); // => 1
// Deprecated: Implicit conversion from float 1.2 to int loses precision
myFunc('-2.5'); // => -2
// Deprecated: Implicit conversion from float-string "-2.5" to int loses precision

// 型エラーになるもの
myFunc('');
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, string given
myFunc([]);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, array given
class Bar { public $x = 1; };
$bar = new Bar();
myFunc($bar);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, Bar given

挙動を見ている感じ暗黙の型変換が行えるかどうかをチェックしているように感じました。
TypeScript などのような型でのチェックができる訳ではなさそうなので注意が必要そうです

結論: オプショナルな引数の初期値を設定した関数の作成

PHP 7 以上であれば関数の仮引数に 型宣言 + デフォルト引数に null を設定して、null 合体演算子を使ってデフォルト値を与えるのが良さそう

<?php
function myFunc(int $x = null): void {
  $a = $x ?? 'default value';
  // ...
}

⚠ ただし PHP の型宣言は型変換が可能なものは通ってしまうので注意が必要

おわり ₍ ᐢ. ̫ .ᐢ ₎

追記 厳密な型チェックの方法を教えてもらいました!


[参考]

お仕事先でこれ読んでますが凄く知見がたまる感があって大変良いです!