PHP
SplFileObject

SplFileObjectのREAD_AHEAD, SKIP_EMPTY, DROP_NEW_LINEはPHPのバージョンによって挙動がバラバラな件

SplFileObjectでCSVを読み込むとき、よく次のオプションを設定するコードを見かける。

$file = new SplFileObject($path);

$file->setFlags(
SplFileObject::READ_CSV |
SplFileObject::READ_AHEAD |
SplFileObject::SKIP_EMPTY |
SplFileObject::DROP_NEW_LINE
);

これらのオプションの説明はSplFileObjectの定数のものをそのまま引用する。



  • SplFileObject::DROP_NEW_LINE 行末の改行を読み飛ばします。


  • SplFileObject::READ_AHEAD 先読み/巻き戻しで読み出します。


  • SplFileObject::SKIP_EMPTY ファイルの空行を読み飛ばします。期待通りに動作させるには、READ_AHEAD フラグを有効にしないといけません。


  • SplFileObject::READ_CSV CSV 列として行を読み込みます。

この説明を読む限り、SKIP_EMPTYDROP_NEW_LINEを使って、空行をスキップするような実装ができそうだし、空行をスキップするためにこれらのオプションを使った実装をよく目にする。

しかし、PHPのバージョンよってこれらオプションの動作が全く異なり、2パターンの振る舞いが確認できた。しかも、特定のバージョンから振る舞いが切り替わったわけではなく、特定のバージョンまでを気にすればいいものではないので厄介だ。

論より証拠に、PHP5.4.0から7.3.1のバージョンで、DROP_NEW_LINEREAD_AHEADSKIP_EMPTYの3オプションの全組み合わせ8とおりを試した結果を提示したい。SplFileObjectには2行目と4行目に空行がある次のデータを持ったファイルを与えた。


data.csv

1<LF>

<LF>
3<LF>

出力結果は次のようになった:

PHP 5.4.0〜5.5.20, 5.6.0〜5.6.4, 7.2.0〜7.3.1

オプション

出力

READ_AHEAD
SKIP_EMPTY
DROP_NEW_LINE
[["1"],["3"]]

READ_AHEAD
SKIP_EMPTY

[["1"],[null],["3"]]

READ_AHEAD

DROP_NEW_LINE
[["1"],[null],["3"]]

READ_AHEAD

[["1"],[null],["3"]]

SKIP_EMPTY
DROP_NEW_LINE
[["1"],["3"]]

SKIP_EMPTY

[["1"],[null],["3"]]

DROP_NEW_LINE
[["1"],[null],["3"]]

[["1"],[null],["3"]]

PHP 5.5.21〜5.5.38, 5.6.5〜5.6.38, 7.0.0〜7.1.25

オプション

出力

READ_AHEAD
SKIP_EMPTY
DROP_NEW_LINE
[["1"],["3"]]

READ_AHEAD
SKIP_EMPTY

[["1"],[null],["3"]]

READ_AHEAD

DROP_NEW_LINE
[["1"],[null],["3"],[null]]

READ_AHEAD

[["1"],[null],["3"],[null]]

SKIP_EMPTY
DROP_NEW_LINE
[["1"],["3"],false]

SKIP_EMPTY

[["1"],[null],["3"],false]

DROP_NEW_LINE
[["1"],[null],["3"],[null]]

[["1"],[null],["3"],[null]]

検証に使ったコード

<?php

$flags = [
'READ_AHEAD' => SplFileObject::READ_AHEAD,
'READ_AHEAD | DROP_NEW_LINE' => SplFileObject::READ_AHEAD | SplFileObject::DROP_NEW_LINE,
'READ_AHEAD | SKIP_EMPTY' => SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY,
'READ_AHEAD | SKIP_EMPTY | DROP_NEW_LINE' => SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY | SplFileObject::DROP_NEW_LINE,
];

$file = new SplTempFileObject();
$file->fwrite("1\n\n3\n");
foreach ([SplFileObject::READ_AHEAD, 0] as $readAhead) {
foreach ([SplFileObject::SKIP_EMPTY, 0] as $skipEmpty) {
foreach ([SplFileObject::DROP_NEW_LINE, 0] as $dropNewLine) {
$file->rewind();
$file->setFlags(
SplFileObject::READ_CSV | $readAhead | $skipEmpty | $dropNewLine
);
printf(
" %s | %s | %s | %s\n", // SKIP_EMPTY
$readAhead ? 'READ_AHEAD' : ' ',
$skipEmpty ? 'SKIP_EMPTY' : ' ',
// DROP_NEW_LINE
$dropNewLine ? 'DROP_NEW_LINE' : ' ',
json_encode(array_values(iterator_to_array($file)))
);
}
}
}

検証環境: Online PHP editor | output for 25GRZ


結論

バージョンによってSplFileObjectのオプションの振る舞いが異なるので、READ_AHEAD, SKIP_EMPTY, DROP_NEW_LINEは避けて空行処理は自前でやるか、DROP_NEW_LINEを使うとセル内の改行が無くなる問題があるため、DROP_NEW_LINEは避けた上でどのバージョンでも振る舞いが同じREAD_AHEADSKIP_EMPTYの組み合わせにしたほうがいい。