In PHP, how do you iterate a CSV file with SplFileObject::READ_CSV, and which flags and setCsvControl() settings avoid blank-row and escape surprises?
answer
- setFlags turns lines into rows
- blank lines need two more flags
- SKIP_EMPTY needs READ_AHEAD
- setCsvControl sets the dialect once
- iteration stays silent about the escape default
basics
~10 sCall setFlags(SplFileObject::READ_CSV | READ_AHEAD | SKIP_EMPTY | DROP_NEW_LINE) so foreach yields parsed rows and skips blank lines, and setCsvControl(',', '"', '') so the backslash escape is off.
solid answer
~40 s`SplFileObject` is an iterator over a file's lines; with the `SplFileObject::READ_CSV` flag each iteration returns a **parsed record** instead of a string, using the dialect stored by `setCsvControl()`. Alone, `READ_CSV` returns a blank line as `[null]`; adding `SKIP_EMPTY | DROP_NEW_LINE` skips it, and the manual says `SKIP_EMPTY` needs `READ_AHEAD` to work as expected, so the usual set is all four flags. The dialect defaults to comma, double quote and backslash escape. The trap: `$file->fgetcsv()` without an escape argument raises the 8.4 `E_DEPRECATED` notice, but `foreach` with `READ_CSV` reads the stored escape silently, so a quiet loop can still use the backslash mechanism. Call `setCsvControl(',', '"', '')` once. Memory stays at one record, like a `fgetcsv()` loop.
code
php · 20 lines<?php
declare(strict_types=1);
$file = new SplFileObject('members.csv');
$file->setFlags(
SplFileObject::READ_CSV
| SplFileObject::READ_AHEAD
| SplFileObject::SKIP_EMPTY
| SplFileObject::DROP_NEW_LINE
);
$file->setCsvControl(';', '"', ''); // dialect set once, escape off
$header = null;
foreach ($file as $row) {
if ($header === null) {
$header = $row;
continue;
}
printf("%s <%s>\n", $row[0], $row[1]);
}go deeper
Recall that setFlags(SplFileObject::READ_CSV) makes foreach return arrays, and that setCsvControl() sets the separator.
Explain which flags skip blank lines, why SKIP_EMPTY needs READ_AHEAD, and how setCsvControl() feeds iteration and the method calls.
Catch the silent-escape case in reviews and make long imports resumable with key() checkpoints, knowing seek() still reads every earlier record.
Choose between SplFileObject iteration and plain handles for a shared import library, balancing composability against explicit per-call dialects.
## SplFileObject as a CSV iterator `SplFileObject` wraps an open file in an object that implements `SeekableIterator` and `RecursiveIterator`, so a `foreach` walks the file line by line. Its behaviour is tuned with **flags** set through `setFlags()`: | Flag | Effect | |---|---| | `SplFileObject::READ_CSV` | each element is a parsed CSV record (`array`) instead of a line | | `SplFileObject::READ_AHEAD` | read on rewind/next | | `SplFileObject::SKIP_EMPTY` | skip empty lines; needs `READ_AHEAD` to work as expected | | `SplFileObject::DROP_NEW_LINE` | drop the trailing newline of each line | The manual for `SplFileObject::fgetcsv()` states that a blank line is returned as an array with a single `null` **unless** `SKIP_EMPTY | DROP_NEW_LINE` is set. That is why the idiomatic set is ```php $file->setFlags( SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY | SplFileObject::DROP_NEW_LINE ); ``` ## The dialect: setCsvControl() and getCsvControl() The separator, enclosure and escape used by `READ_CSV` iteration are stored on the object: - `setCsvControl(string $separator = ",", string $enclosure = "\"", string $escape = "\\"): void` stores them; - `getCsvControl(): array` returns the three current values; - `$file->fgetcsv()` and `$file->fputcsv()` fall back to the stored values when you omit arguments. Separator and enclosure must be exactly one byte, or `setCsvControl()` throws a `ValueError`; escape must be one byte or the empty string. ## The silent escape trap The object starts with the backslash escape and a marker saying the escape is still the default. In the PHP 8.5 source: 1. `$file->fgetcsv()` or `$file->fputcsv()` **without** an escape argument, while the escape is still the default, raises `E_DEPRECATED` telling you to pass it "either explicitly or via SplFileObject::setCsvControl()". 2. `foreach` over a `READ_CSV` file uses the stored escape **without** that check, so the loop is quiet. 3. `setCsvControl()` called **with** an escape argument clears the marker. Called with only a separator (as in the manual's `setCsvControl('|')` example), it raises the same `E_DEPRECATED` notice itself and leaves the escape at the backslash default. So a clean log does not prove the backslash mechanism is off. Set the dialect explicitly: `$file->setCsvControl(',', '"', '')`. ## Iterating with a header - Read the header from the first element, or call `$file->fgetcsv()` once before the loop; - `key()` returns the iterator's current position and `seek($n)` rewinds and reads forward to position `$n`, which helps resume an import that stopped; - rows are still arrays of strings, so validation and type conversion are your job. ## Compared with a plain fgetcsv() loop | Aspect | `fgetcsv()` on a handle | `SplFileObject` + `READ_CSV` | |---|---|---| | Memory | one record | one record | | Blank line | `[null]`, skip by hand | skipped with the flags | | Dialect | passed on every call | stored once with `setCsvControl()` | | Deprecation on omitted escape | always | on method calls only, not on iteration | | Composes with iterators | no | yes, it is an `Iterator` | Both parse with the same engine, so quoting behaviour is identical once the dialect matches. ## Writing through the same object `SplFileObject::fputcsv(array $fields, ...)` writes a record using the stored dialect when you omit the arguments, so one `setCsvControl()` call keeps reading and writing consistent. For CSV that should stay in memory until it grows, `SplTempFileObject` (constructor argument `$maxMemory`, default 2 MB) is a `SplFileObject` backed by `php://temp`, with the same flags and methods. ## Checklist 1. `new SplFileObject($path)` (mode `'r'` by default). 2. `setFlags()` with all four flags. 3. `setCsvControl()` with all three arguments, escape `''`. 4. Iterate, treating the first element as the header.
- Why does setCsvControl('|') alone not settle the 8.4 escape deprecation for an SplFileObject?`setCsvControl()` only marks the escape as chosen when you pass the escape argument. With just a separator, the call itself raises `E_DEPRECATED`, the stored escape stays the backslash default, and a later `$file->fgetcsv()` without an escape raises it again. Pass all three: `setCsvControl('|', '"', '')`.
- How would you resume an interrupted import from line 50,000 with SplFileObject?Checkpoint `key()` as rows are committed, then call `seek()` with that value, using the same flags, before iterating again; `SplFileObject` implements `SeekableIterator`. `seek()` rewinds and reads forward element by element, so it saves reprocessing, not reading; for very large files store an `ftell()` byte offset instead.
saying these in an interview costs you the question
- Thinking READ_CSV alone skips blank lines
- Setting SKIP_EMPTY without READ_AHEAD and expecting it to work
- Taking a silent READ_CSV loop as proof the escape is disabled
- Believing setCsvControl('|') also turns off the backslash escape
- Assuming SplFileObject loads the whole file into memory