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

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

```php
$file = new SplFileObject($path);
$file->setFlags(
    SplFileObject::READ_CSV | 
    SplFileObject::READ_AHEAD | 
    SplFileObject::SKIP_EMPTY | 
    SplFileObject::DROP_NEW_LINE
);
```

これらのオプションの説明は[SplFileObjectの定数](http://php.net/manual/ja/class.splfileobject.php#splfileobject.constants)のものをそのまま引用する。


* `SplFileObject::DROP_NEW_LINE` 行末の改行を読み飛ばします。
* `SplFileObject::READ_AHEAD` 先読み/巻き戻しで読み出します。
* `SplFileObject::SKIP_EMPTY` ファイルの空行を読み飛ばします。期待通りに動作させるには、`READ_AHEAD` フラグを有効にしないといけません。
* `SplFileObject::READ_CSV` CSV 列として行を読み込みます。

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

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

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

```text: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"]<font color=red>,[null]</red>]
 READ_AHEAD |            |               | [["1"],[null],["3"]<font color=red>,[null]</red>]
            | SKIP_EMPTY | DROP_NEW_LINE | [["1"],["3"]<font color=red>,false</red>]
            | SKIP_EMPTY |               | [["1"],[null],["3"]<font color=red>,false</red>]
            |            | DROP_NEW_LINE | [["1"],[null],["3"]<font color=red>,[null]</red>]
            |            |               | [["1"],[null],["3"]<font color=red>,[null]</red>]

検証に使ったコード

```php
<?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](https://3v4l.org/25GRZ)

## 結論

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