<?php
declare(strict_types=1);
namespace App\Services\Ingestion;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Facades\DB;
use Generator;
use RuntimeException;
final class StreamCsvIngestionService
{
public function import(string $filePath, int $chunkSize = 1000): int
{
if (!file_exists($filePath) || !is_readable($filePath)) {
throw new RuntimeException("File unreadable: {$filePath}");
}
$totalProcessed = 0;
LazyCollection::make(function () use ($filePath): Generator {
$handle = fopen($filePath, 'rb');
$header = fgetcsv($handle, 0, ',');
while (($row = fgetcsv($handle, 0, ',')) !== false) {
if (count($row) === count($header)) {
yield array_combine($header, $row);
}
}
fclose($handle);
})
->chunk($chunkSize)
->each(function ($chunk) use (&$totalProcessed): void {
$records = $chunk->map(fn (array $row): array => [
'account_number' => trim((string)$row['account_no']),
'transaction_ref'=> trim((string)$row['txn_ref']),
'amount' => (float)$row['amount'],
'currency' => strtoupper(trim((string)$row['currency'])),
'status' => 'processed',
'synced_at' => now(),
'created_at' => now(),
'updated_at' => now(),
])->toArray();
DB::table('financial_transactions')->upsert(
$records,
['transaction_ref'],
['amount', 'status', 'synced_at', 'updated_at']
);
$totalProcessed += count($records);
});
return $totalProcessed;
}
}
Architecting Zero-Memory Streaming for Millions of Records
A classic rite of passage for enterprise backend engineers is the 100 Lines Challenge: writing a concise, single-purpose file that solves a complex engineering problem without relying on massive third-party dependencies.
The Problem: Memory Exhaustion During Large File Import
When importing CSV files containing 500,000+ records, standard Eloquent collection mapping quickly exceeds PHP's memory_limit (128MB/256MB) and crashes with fatal Out of Memory errors.
The Solution: Lazy Collections & Chunked Bulk Upserts
- Generator Streams: Read rows line-by-line using
fopen()andfgetcsv()wrapped in Laravel'sLazyCollection. Memory usage stays flat under 12MB. - Batch Chunking: Aggregate parsed rows into memory chunks of 1,000 records.
- Raw SQL Bulk Upsert: Execute
DB::table('records')->upsert()with an indexed composite key.
Accelerate Your Engineering Career
Get monthly masterclasses, exclusive code snippets, and direct architectural reviews.
Become a Supporter • KES 500 / Month