bcncommerce/json-stream
Stream JSON reading and writing for PHP. Incrementally parse or generate large JSON documents from file handles without loading everything into memory. Supports entering/leaving objects and arrays, reading keys or iterating items—ideal for exports/imports like product catalogs.
Installation:
composer require bcncommerce/json-stream
Add to composer.json if using a monorepo or custom package.
First Use Case:
Replace a memory-heavy json_encode() call for a large dataset (e.g., exporting 100K+ records).
use Bcncommerce\JsonStream\Writer;
$fh = fopen('large_export.json', 'w');
$writer = new Writer($fh);
$writer->enter(Writer::TYPE_OBJECT);
foreach ($hugeArray as $item) {
$writer->write(null, $item); // Streams incrementally
}
$writer->leave();
fclose($fh);
Where to Look First:
Writer class for generating JSON.Reader class for parsing JSON.Streaming Exports:
public function handle() {
$products = Product::all();
$fh = fopen(storage_path('app/exports/products.json'), 'w');
$writer = new Writer($fh);
$writer->enter(Writer::TYPE_ARRAY);
foreach ($products as $product) {
$writer->write(null, $product->toArray());
}
$writer->leave();
fclose($fh);
}
Streaming Imports:
public function handle() {
$fh = fopen(storage_path('app/uploads/large_payload.json'), 'r');
$reader = new Reader($fh);
$reader->enter(Reader::TYPE_ARRAY);
while ($item = $reader->read()) {
// Process $item in chunks (e.g., save to DB)
Model::create($item);
}
$reader->leave();
fclose($fh);
}
API Responses:
public function show(Request $request) {
$fh = fopen('php://output', 'w');
$writer = new Writer($fh);
$writer->enter(Writer::TYPE_OBJECT);
$writer->write('data', $this->streamDataGenerator());
$writer->leave();
return response()->stream(fn() => $fh);
}
private function streamDataGenerator() {
foreach ($largeDataset as $item) {
yield $item;
}
}
Hybrid with Laravel Storage:
use Illuminate\Support\Facades\Storage;
$writer = new Writer(Storage::disk('s3')->open('exports/large.json', 'w'));
// ... write logic ...
$writer->leave();
ETL Pipelines:
$reader = new Reader(fopen('input.csv', 'r')); // Custom CSV parser
$writer = new Writer(fopen('output.json', 'w'));
$writer->enter(Writer::TYPE_ARRAY);
while ($row = $reader->read()) {
$writer->write(null, $this->transformRow($row));
}
$writer->leave();
Real-Time Processing:
public function handle(WebhookPayload $payload) {
$fh = fopen('php://temp', 'r+');
fwrite($fh, $payload->getContent());
rewind($fh);
$reader = new Reader($fh);
$reader->enter(Reader::TYPE_OBJECT);
$data = $reader->read('data'); // Parse incrementally
$reader->leave();
// Process $data
}
Testing:
php://memory for unit tests.
$fh = fopen('php://memory', 'r+');
$writer = new Writer($fh);
$writer->enter(Writer::TYPE_OBJECT);
$writer->write('test', 'value');
$writer->leave();
rewind($fh);
$reader = new Reader($fh);
$result = $reader->read('test');
$this->assertEquals('value', $result);
Laravel Facades:
// app/Facades/JsonStream.php
public static function export(array $data, string $path) {
$fh = Storage::disk('public')->open($path, 'w');
$writer = new Writer($fh);
$writer->enter(Writer::TYPE_OBJECT);
foreach ($data as $key => $value) {
$writer->write($key, $value);
}
$writer->leave();
fclose($fh);
}
Service Providers:
$this->app->singleton('json.stream.writer', function () {
return new Writer(fopen('php://temp', 'w'));
});
Middleware:
public function handle($request, Closure $next) {
if ($request->wantsJson() && $request->largePayload) {
$response = $next($request);
$response->setContent($this->streamJson($response->getContent()));
}
return $next($request);
}
private function streamJson($data) {
$fh = fopen('php://temp', 'r+');
$writer = new Writer($fh);
$writer->enter(Writer::TYPE_OBJECT);
foreach ($data as $key => $value) {
$writer->write($key, $value);
}
$writer->leave();
rewind($fh);
return stream_get_contents($fh);
}
Queue Jobs:
public function handle() {
$fh = fopen(storage_path('app/logs/large.log.json'), 'r');
$reader = new Reader($fh);
$reader->enter(Reader::TYPE_ARRAY);
while ($logEntry = $reader->read()) {
$this->processLogEntry($logEntry);
}
$reader->leave();
fclose($fh);
}
Resource Leaks:
fclose() or use finally blocks.
$fh = fopen('file.json', 'r');
try {
$reader = new Reader($fh);
// ... read logic ...
} finally {
fclose($fh);
}
Storage::disk()->close() for Laravel’s storage facades.Memory Spikes:
// Anti-pattern: Memory spike
$writer->write(null, $hugeArray);
// Pattern: Memory-efficient
$writer->enter(Writer::TYPE_ARRAY);
foreach ($hugeArray as $item) {
$writer->write(null, $item);
}
$writer->leave();
Nested Structures:
enter()/leave(): Causes malformed JSON. Validate with a tool like JSONLint.
// Example of mismatch
$writer->enter(Writer::TYPE_OBJECT);
$writer->enter('items', Writer::TYPE_ARRAY); // Enter array
// ... missing leave for array ...
$writer->leave(); // Only leaves object, not array
PHP Version Quirks:
null values in read().
// Workaround for PHP 8.2+
$value = $reader->read('key') ?: null;
Encoding Issues:
fopen($path, 'w+') with explicit encoding.
$fh = fopen('file.json', 'w+');
stream_set_blocking($fh, true);
How can I help you explore Laravel packages today?