cerbero/lazy-json
Lazy JSON is a lightweight PHP package for memory-efficient parsing of large JSON documents. Instead of decoding an entire payload at once, it lets you traverse JSON lazily, reducing memory usage and making big files or streams easier to process.
Install with Composer:
composer require cerbero/lazy-json
In Laravel the service provider and Json facade are auto-discovered. In a non-Laravel app, either bootstrap the facade manually or call the underlying Cerbero\LazyJson\LazyJson class directly.
The first and most common use case is turning a large JSON export into a LazyCollection so you can process records one at a time instead of json_decode-ing the whole file into memory:
use Cerbero\LazyJson\Facades\Json;
Json::fromFile(storage_path('app/exports/orders.json'))
->map(fn (array $order) => $order['id'])
->take(10)
->all();
Where to look first:
README.md — the source methods and pointer syntax.src/LazyJson.php — the static fromFile, fromUrl, fromStream, fromString entry points.src/Facades/Json.php and the service provider — what boots in Laravel.json-machine, so familiarity with that package pays off for edge cases.Your first real task is almost always: pick a source, optionally pass a JSON Pointer to drill into the array you care about, and iterate.
Pick the cheapest source for the job. All four entry points return a LazyCollection, so downstream code is identical regardless of origin:
Json::fromFile('/data/users.json');
Json::fromUrl('https://example.com/feed.json');
Json::fromStream(fopen('php://stdin', 'r'));
Json::fromString($json);
Target a nested array with a pointer. Don't stream the whole envelope when you only need one branch. Pointers use JSON Pointer syntax (RFC 6901, leading slash, array indexes as integers):
Json::fromFile('response.json', '/data/items')
->each(fn (array $item) => process($item));
This skips the header metadata and any sibling arrays, which is where the biggest CPU/memory wins come from on huge API dumps.
Stream into the database in batches. Avoid one insert per row; use chunk so you keep backpressure and stay inside memory limits:
Json::fromFile('orders.json', '/orders')
->chunk(1000)
->each(fn ($chunk) => Order::insert($chunk->values()->all()));
For upserts or per-row logic where you still want batching semantics, use ->each on the chunk instead of insert.
Fan out into queues or jobs. Because iteration is lazy, you can dispatch without loading the file:
Json::fromFile('import.json', '/rows')
->each(fn (array $row) => ImportRow::dispatch($row));
Pair this with a rate-limited queue connection when the file has millions of rows.
Compose with normal LazyCollection operators. map, filter, flatMap, skip, take, tap, takeUntil, and chunk all work as expected — but treat anything that materializes the collection (all, collect, toArray, remember) as a deliberate last step.
Add progress reporting in Artisan commands. tap between the source and the sink is the cleanest hook:
$bar = $this->output->createProgressBar();
Json::fromFile($path, '/rows')
->tap(fn () => $bar->advance())
->each(fn ($row) => import($row));
$bar->finish();
Test with fromString instead of fixtures on disk. It keeps tests hermetic and lets you exercise pointer edge cases (empty arrays, missing keys, mixed object/array structures) without touching the filesystem.
Prefer fromStream when you need HTTP control. fromUrl is fine for simple cases, but if you need custom headers, timeouts, or retries, open the stream yourself with Guzzle/Laravel Http and hand the resource to fromStream.
LazyCollection looks reusable, but the underlying generator/stream is consumed after the first full iteration. If you need the data twice, recreate the collection from the source (cheap for files) or cache explicitly with remember() and accept the memory cost.count() and other terminal calls consume the data. Calling ->count() and then iterating will yield nothing on the second pass. If you need a count, do it in the same pass or from the source metadata.fromStream if your sources are heterogeneous.try/catch and expect JsonMachine syntax/invalid-argument exceptions rather than something specific to this package. Log the source and the pointer when catching — they're the two things you'll need to reproduce.json_decode on individual records.->all(), ->collect(), ->toArray(), and ->remember() all defeat the purpose. A dd($collection) on a multi-GB file will also try to render everything — use ->take(5)->all() while debugging.fromUrl uses PHP stream wrappers, so network stalls look like hangs. For production, wrap Guzzle with explicit timeouts and feed fromStream, and always catch for HTTP 4xx/5xx bodies being parsed as JSON.json-machine directly (JsonMachine::fromFile(..., new ExtJsonDecoder(true))) — this package is a thin convenience layer over it.composer.json.json-machine or a custom parser later without touching call sites.How can I help you explore Laravel packages today?