PHPのarray_mapにキーが引き継がれる条件と、配列を2つ渡すと連想配列のキーが消える理由
array_mapは、配列の各要素にコールバックを適用して新しい配列を返す関数です。配列を1つだけ渡すとキーが保たれ、2つ以上渡すとキーが捨てられて0からの連番になります。
連想配列をarray_mapで変換して、念のためコールバックの引数を増やしたら、返ってきた配列のキーがごっそり消えていた。そんなことが私にはありました。エラーも警告も出ないので、気づくのが遅れるんですよね。
array_mapは配列1つのときキーを保つのか
保ちます。文字列キーはもちろん、飛び飛びの整数キーもそのまま引き継がれます。array_filterがキーを保つのは有名ですが、array_mapも条件つきで同じ動きをします。
$p = ['a' => 1, 'b' => 2];
echo json_encode(array_map(fn($x) => $x * 10, $p));
// {"a":10,"b":20}
echo json_encode(array_map(fn($x) => $x, [3 => 'x', 7 => 'y']));
// {"3":"x","7":"y"}
元の配列とキーが対応したまま返ってくるので、連想配列の値だけを加工したいときに楽です。ただし整数キーが飛んでいる配列だと、json_encodeでオブジェクトになる点は覚えておいたほうがいいと思います。
なぜ配列を2つ渡すとキーが消えるのか
複数の配列を渡すと、array_mapは各配列の同じ位置の要素をまとめてコールバックに渡す動きになります。このとき、どの配列のキーを採用するかは決まりようがないので、結果は0から始まる連番の配列になります。
$p = ['a' => 1, 'b' => 2];
echo json_encode(array_map(fn($x, $y) => $x * 10, $p, $p));
// [10,20]
コールバックが第2引数を使っていなくても、配列を2つ渡した時点で連番になります。「使わない引数だから足しても影響しない」と考えるとハマります。
キーも一緒にコールバックへ渡すにはどう書くのか
array_mapにはキーを渡す仕組みがないので、array_keysで取り出したキーの配列を、もう1つの配列として渡すのが定番です。結果は連番になるため、キーを戻したいならarray_combineで組み直します。
$p = ['a' => 1, 'b' => 2];
echo json_encode(array_map(fn($k, $v) => "$k=$v", array_keys($p), $p));
// ["a=1","b=2"]
$keys = array_keys($p);
$mapped = array_map(fn($k, $v) => "$k:$v", $keys, $p);
echo json_encode(array_combine($keys, $mapped));
// {"a":"a:1","b":"b:2"}
array_keysと元の配列は同じ順序で並ぶので、位置のずれを心配しなくて済みます。キーを使う変換が何度も出てくるなら、素直にforeachで書いたほうが読みやすいこともあります。
長さの違う配列を渡すとどうなるのか
短いほうの配列は、足りない分がnullで埋められます。エラーにはならず、長いほうの長さに合わせた結果が返ります。
echo json_encode(array_map(fn($x, $y) => [$x, $y], [1, 2], [10]));
// [[1,10],[2,null]]
長さがそろっている前提で書いたコードに、片方だけ短い配列が混ざると、nullが静かに紛れ込みます。あとで型エラーや計算結果のずれとして表に出てくるので、入力の長さを先に確認しておくと安全です。
コールバックにnullを渡すと何が起きるのか
配列が1つならそのまま返り、2つ以上なら、各位置の要素を並べた配列が返ります。いわゆるzipの動きで、長さが違えばやはりnullで埋まります。
echo json_encode(array_map(null, ['a' => 1, 'b' => 2]));
// {"a":1,"b":2}
echo json_encode(array_map(null, [1, 2], ['a', 'b', 'c']));
// [[1,"a"],[2,"b"],[null,"c"]]
複数の配列を縦横に入れ替えたいときに、ひと手間かけずに書けます。ただし、配列が1つのときは何も変換されない点は、知らないと混乱するところかもしれません。
まとめ
array_mapは、配列を1つだけ渡せばキーを保ち、2つ以上渡せば連番になります。キーを使いたいときはarray_keysを一緒に渡して、必要ならarray_combineで戻す、という流れで当面は足りる気がしています。引数を1つ足しただけでキーが変わる関数は、他にあまり思いつきません。
よくある質問
Q. array_mapで連想配列のキーを保ったまま値を変換するには?
A. 配列を1つだけ渡せば、キーはそのまま保たれます。コールバックでキーも使いたいときは、array_keysの結果と元の配列を渡し、array_combineでキーを戻します。
Q. array_mapの結果のキーを0からの連番に振り直すには?
A. 結果にarray_valuesを通します。リストとしてjson_encodeしたい場合は、この一手間でオブジェクトではなく配列として出力されます。
Q. array_mapとarray_walkはどう使い分けますか?
A. array_mapは変換後の新しい配列を返し、array_walkは参照渡しで元の配列を書き換えます。キーも受け取れるのはarray_walkですが、値を変換した配列がほしいだけならarray_mapのほうが副作用が少なく読みやすいです。