Back to All Nuggets
Laravel php ⏱️ 2 min read
Production Implementation
Ask endoAI
<?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() and fgetcsv() wrapped in Laravel's LazyCollection. 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