PHPのarray_combineは要素数が違うとValueErrorになる CSVの行ずれと重複キーで値が消える落とし穴

array_combine()は、1つ目の配列をキー、2つ目の配列を値にして連想配列を作る関数です。PHP 8.0以降は要素数が違うとValueErrorを投げ、重複するキーは後の値で黙って上書きされます。

CSVの見出し行と各行を組み合わせるときの定番ですが、1行だけ列数が足りないデータが混ざった途端に処理全体が落ちる、ということがあります。私も取り込みバッチで一度やらかして、深夜に原因を探した覚えがあります。

要素数が違うとPHP 8.0以降はValueErrorになる

PHP 7系までは警告を出してfalseを返す挙動でした。8.0からは例外になったので、戻り値のfalseを確認する古い書き方は意味を失っています。例外を握る側に回る必要があります。

<?php
$header = ['id', 'name', 'email'];

try {
    array_combine($header, [1, 'のり']);
} catch (ValueError $e) {
    echo $e->getMessage();
    // array_combine(): Argument #1 ($keys) and argument #2 ($values) must have the same number of elements
}

落ちること自体は悪いことではなく、ずれたデータが静かに混ざるよりはずっとましです。ただ、1行の不備で全件が止まるのは困る場面も多いと思います。

CSVの不正行だけを飛ばすにはどう書く?

行ごとにtry/catchで囲むか、事前にcount()で比べる方法があります。どちらが良いかは、不正行を記録して続けたいのか、即座に止めたいのかで決まる気がします。

<?php
$csv = "id,name,email\n1,のり,n@example.com\n2,bob\n";
$lines = explode("\n", trim($csv));
$head = str_getcsv(array_shift($lines));

foreach ($lines as $no => $line) {
    $row = str_getcsv($line);
    if (count($row) !== count($head)) {
        error_log("line " . ($no + 2) . ": 列数が一致しません");
        continue;
    }
    $assoc = array_combine($head, $row);
    // ['id' => '1', 'name' => 'のり', 'email' => 'n@example.com']
}

不正行を行番号つきで記録できるので、あとで元データを直すときに助かります。なお、ここでは説明用に文字列を直接分割していますが、実際のCSVにはセル内改行もあり得るので、本番ではfgetcsv()かSplFileObjectで読むほうが安全です。

足りない列をnullで埋めたいときはarray_padを使う

末尾の列が空のまま省略されるタイプのデータなら、不正行として弾くより埋めてしまうほうが実情に合うことがあります。array_pad()で長さをそろえてから結合します。

<?php
$header = ['id', 'name', 'email'];
$row = [1, 'x'];

$padded = array_pad($row, count($header), null);
print_r(array_combine($header, $padded));
// id => 1, name => x, email => (null)

ただし、行が多すぎる場合はarray_pad()では縮まないので、その場合はValueErrorになります。どこまでを許容するかは、取り込み元の仕様と相談になりそうです。

重複するキーは後の値で上書きされて消える

要素数が合っていても安心はできません。キーの配列に同じ値が2つあると、エラーにも警告にもならず、あとの値だけが残ります。見出し行に同じ列名が2つあるCSVで起きがちです。

<?php
print_r(array_combine(['a', 'b', 'a'], [1, 2, 3]));
// a => 3, b => 2  (1は消える)

気づかないまま3列が2列になるので、件数チェックだけでは見逃します。見出しの重複は、結合の前にcount(array_unique($head)) !== count($head)で確かめておくのが無難です。

キーに使った値は文字列か整数に変換される

値のほうは何でも入れられますが、キーに使われる配列の要素は、配列キーのルールで変換されます。たとえば文字列の’1’とtrueはどちらも整数の1になり、これも重複として上書きされます。

<?php
var_dump(array_combine(['01', '1', 1.5, true, null], [1, 2, 3, 4, 5]));
// "01" => 1, 1 => 4 ('1'とtrueが衝突), "1.5" => 3, "" => 5

文字列’01’は整数にならずそのまま残る一方、nullは空文字のキーになります。キーに使う配列は、文字列の列名だけを入れるものとして扱うのが安全です。

まとめ

array_combine()は便利ですが、PHP 8.0以降は要素数の不一致が例外になり、重複キーは黙って消えるという2つの性質を覚えておけば、だいたい事故は防げます。外から来るデータを結合するときは、件数と重複を先に見ておくのが、地味ですが効く気がしています。

よくある質問

Q. array_combineは要素数が違うとfalseを返しますか?
A. PHP 7系まではfalseを返していましたが、PHP 8.0以降はValueErrorを投げます。falseを前提にした判定は動きません。

Q. キーが重複しているとどうなりますか?
A. エラーにはならず、後ろの値で上書きされて先の値は消えます。結合前にarray_unique()で件数を比べると検知できます。

Q. 空配列同士を渡すとエラーになりますか?
A. なりません。要素数が同じ(どちらも0)なので、空の配列が返ります。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です