再帰関数はネストしたオブジェクトや配列を処理する際に便利です。便利ではあるのですが、自身を呼び出す都合上その関数の引数の型は最初に呼ばれる時の型と再度呼び出される時の型の二種類を想定する必要があります。PHP の型機能のみでは、再帰関数以外から最初に呼ぶ時は引数が受け付けるのは array のみ、再帰時の呼出し時には array 以外も受け付ける、といった型を引数につけることはできません。この問題は次の様なコードで解決できます。
if (! function_exists('array_diff_key_recursive')) {
/**
* array_diff_key をネストした配列にも使える様にしたもの。
* a にあって b にないものを残す。
* 呼出し時用の型は PHP の機能で厳密に定義。ここでは array 同士の差分を取得したいので両方とも array 指定
* @param array $a
* @param array $b
* @return array
*/
function array_diff_key_recursive(array $a, array $b): array
{
// 実際に再帰を用いたメイン処理を無名関数で呼び出される関数の内部に記述
// こちらは再帰用の型を引数に取るため array 以外も受け付ける
// ある無名関数を無名関数自身の中で呼ぶにはリファレンス渡しで use する必要がある
$array_diff_key_recursive_inner = static function ($_a, $_b) use (&$array_diff_key_recursive_inner): array {
$ret = [];
// a を基準に配列全体を回す
foreach ($_a as $ak => $av) {
if (!is_array($_b) || !array_key_exists($ak, $_b)) {
// a にあって b にないキーであるか、転じて返すべき部分であるか否か
$ret[$ak] = $av;
} elseif (is_array($av)) {
// 今使っている値が配列ならば更に奥まで見に行く必要があるので再帰
$subRet = $array_diff_key_recursive_inner($av, $_b[$ak]);
if (!empty($subRet)) {
// 再帰した結果、返すべき部分が存在するならば返り値に追加
$ret[$ak] = $subRet;
}
}
}
return $ret;
};
return $array_diff_key_recursive_inner($a, $b);
}
}
再帰関数の処理を無名関数にまとめ、呼び出し用の関数でラッピングします。無名関数で再帰するためにリファレンス渡しで無名関数を格納する変数を無名関数自身に渡しています。
PHP: 無名関数 – Manual
PHP: リファレンス渡し – Manual
リファレンス渡しを用いない通常の use を使った無名関数では、次の様に無名関数が定義された時点で use 対象の変数内の値が固定されます。
// リファレンス渡しなし
$useVal = 1;
$fn = static function() use ($useVal){
var_dump($useVal);
};
$useVal = 2;
$fn(); // int(1)
一方でリファレンス渡しを使うと後から変更ができます。
// リファレンス渡しあり
$useVal = 1;
$fn = static function() use (&$useVal){
var_dump($useVal);
};
$useVal = 2;
$fn(); // int(2)
そのためリファレンス渡しを用いて use する変数を無名関数を定義した時点では未解決にし、use する変数に後から定義した無名関数を代入します。
この様にすると最初の呼出し時と再帰時の呼出し時の二種類の引数の型をそれぞれ PHP の機能によって定義できます。再帰時の型を知ることができるのは内部実装を見た時のみ、最初の呼出し時の型は関数定義の呼出しですぐ見られる(PhpStorm の Ctrl+Qとか)というのはなかなか便利です。