Close a file even when CSV processing throws an exception
A CSV import opens a file and processes rows until a validation error throws. If cleanup exists only after the loop, it is skipped on that error path. Resource cleanup belongs in a finally block.
Separate processing from cleanup
<?php
$stream = fopen('php://temp', 'w+');
if ($stream === false) throw new RuntimeException('Cannot open stream');
try {
fwrite($stream, "sku,quantity\nAX-1,2\n");
rewind($stream);
$header = fgetcsv($stream, escape: '');
assert($header === ['sku', 'quantity']);
} finally {
fclose($stream);
}
assert(!is_resource($stream));
The cleanup executes whether the try block completes or throws. Keep it small. Throwing a new exception or returning a value from finally can obscure the original outcome, making the real import failure harder to diagnose.
Cleanup is not rollback
Closing a stream does not undo database changes already made for earlier rows. Decide whether the import is atomic, chunked, or intentionally partial. Record enough progress for a retry to know which work already succeeded.
For a remote download, avoid holding a database transaction open while waiting for the network. Obtain and validate the input first, then perform appropriately bounded writes. A file lifecycle and a transaction lifecycle often have different boundaries.
Test an exception midway through processing and verify that resources are closed and the recorded import state is correct. A happy-path row count does not cover cleanup. Temporary files that survive a failure should have an explicit retention and removal policy, particularly when they contain customer data.
Reference
Official documentation. The examples here illustrate the stated boundary; adapt them to your application and test its failure paths.