A record is not necessarily a physical line
A fictional operations file has id,status,amount columns. A quoted description can contain commas and, under other CSV contracts, even a newline. Therefore, explode on a line does not replace a CSV parser. Declare delimiter, enclosure, escape, and encoding as part of the producer-consumer contract. This lesson’s dialect uses an explicitly empty escape and doubled internal quotes. Omitting that argument is deprecated from PHP 8.4 according to the documentation; local tests for this expansion use PHP 8.3.17. Separate parsing from validation: obtaining three fields does not establish types, ranges, identity, or permission to import them.
Validate before assigning columns
Before reading data by fixed positions, compare the header with the agreed sequence. If the producer sends amount,id,status, the field count is correct but assignment would be wrong. Two policies are possible: reject differing order or build a name-based mapping after checking missing and duplicate names. Choose one and test it. Handle a blank [null] row under an explicit rule rather than confusing it with false. For each record, check field count before combining values with names. Retain a record number and rejection category without copying every value into logs. A rejection report should support correcting the batch without exposing unnecessary data.
Memory also depends on the consumer
A generator produces values when its consumer advances. This allows one record at a time to be read and validated, but does not guarantee constant memory under every design. Calling iterator_to_array in the consumer accumulates the whole collection again. If one field can occupy hundreds of megabytes, the current record also remains a problem. Define file and record limits in an ingestion layer appropriate to the dialect; blindly splitting CSV at newlines can cut a valid field. To understand behavior, start with three small records, observe when each is produced, and only then use a larger synthetic file. Measure the entire chain, including results, rejections, and logs retained in memory.
Resource ownership and deferred failures
Creating a Generator does not execute the whole import in advance. An exception can appear on the second advance, so a try surrounding construction alone does not cover later consumption. Place handling at the boundary that traverses the reader and can decide whether the batch must stop. The example assigns the stream to the caller: it opens, iterates, and closes it in finally. The generator only uses the borrowed resource. Thus a break or consumer exception does not leave closing responsibility ambiguous. Do not resume a generator after its stream is closed. If a generator owns the resource, retaining a suspended reference requires additional care over cleanup timing.
Writing, replacement, and batch outcome
An old report can disappear before its first replacement line is written: opening an existing destination in mode w truncates it. Validate first and prepare output in a temporary file in the controlled directory. The publication design must then define concurrency, permissions, replacement, and durability for the actual filesystem; do not assume every rename between destinations is atomic. On streams with partial writes, track the bytes actually written and continue only with the pending suffix. Explicitly handle false and zero progress, using bounded waiting if the stream warrants it. Closing a file neither confirms a business transaction nor makes external effects already sent during the batch reversible.
Laboratory and acceptance criteria
Run the example with two valid records and predict the sum: 120 plus 30 gives 150 integer units. Then change header order, remove a column, and insert a blank line. The first and second changes should fail; the blank line is skipped under explicit policy. The reader teaches structure, not a complete financial importer: persistence would additionally require value validation, duplicate control, authorization, and a partial-batch policy. Also compare direct consumption with conversion to an array. The operational summary should report accepted, rejected, and committed records; do not confuse records read with operations actually completed. Connect this reasoning to the transactions and outbox lesson.
<?php
declare(strict_types=1);
function records($stream): Generator {
$header = fgetcsv($stream, null, ',', '"', '');
if ($header!== ['id', 'status', 'amount']) {
throw new UnexpectedValueException('Unexpected header');
}
while (($row = fgetcsv($stream, null, ',', '"', ''))!== false) {
if ($row === [null]) { continue; }
if (count($row)!== 3) {
throw new UnexpectedValueException('Expected three columns');
}
yield array_combine($header, $row);
}
if (!feof($stream)) { throw new RuntimeException('Read failed'); }
}
$stream = fopen('php://memory', 'w+');
if ($stream === false) { throw new RuntimeException('Open failed'); }
try {
$input = "id,status,amount\nA1,ready,120\nA2,ready,30\n"
if (fwrite($stream, $input)!== strlen($input)) {
throw new RuntimeException('Fixture write incomplete');
}
rewind($stream);
$total = 0;
foreach (records($stream) as $row) {
if (!preg_match('/^[0-9]{1,6}$/D', $row['amount'])) {
throw new UnexpectedValueException('Expected bounded integer units');
}
$total += (int) $row['amount'];
}
echo $total;
} finally {
fclose($stream);
}
A batch reads the expected number of lines but publishes amounts swapped with IDs. Header validation would have stopped wrong assignments before the first operation.
Common pitfalls
Avoid accumulating the generator into an array, treating zero as EOF, ignoring headers, closing another component’s resource, or assuming w retains the previous version.
Related topics: Explicit types and comparisons · Intentional arrays and functions · PDO transactions and partial failures
Incremental processing requires a record contract, input limits, disciplined consumption, and explicit resource ownership.
Reference: PHP manual: fgetcsv · PHP 8.5 reference; DR PHP 2026.2; new fixtures executed on PHP 8.3.17