Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Resource Stream Laravel Package

phpro/resource-stream

phpro/resource-stream is a lightweight PHP library for wrapping resource handles in a consistent stream API. It simplifies reading, writing, seeking, and inspecting file, socket, or in-memory resources, making resource-based I/O easier to abstract and test.

View on GitHub
Deep Wiki
Context7

Getting Started

phpro/resource-stream is a plain Composer library — install with composer require phpro/resource-stream and there is nothing to publish or register in Laravel. The whole value proposition is: get a PHP resource (not a string) for a data source so you can copy it around without loading it into memory.

Where to look first:

  1. src/ — the factories are the entry points. Everything else (streams, decorators) is plumbing you rarely touch.
  2. The new ZipStream factory (added in 1.4.0) — this is the first feature that most Laravel apps will actually reach for, because "read one file out of an uploaded zip" is a recurring chore.

The canonical first use case: a user uploads a 200 MB zip containing a CSV, and you want to import the CSV without ever extracting it to storage/app and without reading it into memory.

// Shape of the call — confirm exact factory method names in src/ before committing.
$resource = ZipStream::entry($request->file('archive')->getRealPath(), 'data/report.csv');

// Hand the resource to something that already speaks streams:
Storage::disk('local')->writeStream('imports/report.csv', $resource);

If you already have a resource from Laravel (Storage::readStream(), fopen(), an HTTP response body), you can often skip the factories entirely and use the package's stream wrapper to pass it into typed code. Start by checking what the factory gives you versus what you already have.

Implementation Patterns

Stream into storage instead of file_get_contents. Every Laravel disk method has a streaming sibling: writeStream, putStream, readStream. The package's job is producing the left-hand side of those calls. This is the pattern that pays for the dependency on day one — a 500 MB S3 → local copy becomes constant-memory.

Zip entry → CSV import pipeline. The most common composition with the new 1.4.0 factory:

$resource = ZipStream::entry($zipPath, 'report.csv');

$reader = League\Csv\Reader::createFromStream($resource);
$reader->setHeaderOffset(0);

foreach ($reader->getRecords() as $offset => $record) {
    $rows[] = $record;

    if (count($rows) === 500) {
        DB::transaction(fn () => ImportRow::upsert($rows, ['external_id']));
        $rows = [];
    }
}

The zip never touches disk, and memory stays flat at ~500 rows regardless of archive size.

Stream downloads back out. response()->streamDownload(fn () => fpassthru($resource), 'report.csv') closes the loop — you can serve a file that came out of a zip, an S3 object, or a generated report with the same code path. Pass the closure, not the resource, so the response can be prepared before the body opens.

Keep streams out of queued payloads. Streams are not serializable. Pass a disk path or an ID into the job, open the stream inside handle(), and close it in a finally. A job that receives a resource will fail at dispatch time with a confusing serialization error.

Testing. Storage::fake('local') works fine because the package deals in resources, which Flysystem's fake adapter accepts. Build fixtures with ZipArchive in setUp() (the zip extension is required anyway if you use ZipStream), then assert with stream_get_contents(Storage::readStream(...)). Add one test that asserts peak memory (memory_get_peak_usage) if the whole point of the change is streaming.

Integration tip. Wrap the factory calls in a small service or a container binding so tests can swap in an in-memory stream (fopen('php://memory', 'r+')) without touching zip fixtures.

Gotchas and Tips

Streams are single-use and often non-seekable. The classic bug: call stream_get_contents($resource) twice and get an empty string the second time. Either rewind() (only works on seekable streams) or recreate the stream from the factory. Treat every factory call as producing a fresh, forward-only cursor.

Always close. fclose() in a finally, or the package's close() method if one exists on the stream object. streamDownload closes for you; Storage::writeStream does not, and long-running queue workers will leak file descriptors across thousands of jobs.

"ZipStream" is lazy on read, not on index. PHP's zip support reads the central directory up front, so you get entry-level laziness but not byte-level laziness on the archive index. Don't open hundreds of entries from a huge archive in one request; take the ones you need and close them.

Validate entry names and sizes on user uploads. The new factory makes it trivial to pull an arbitrary entry out of a user-supplied archive — which means entry-name traversal (../../) and zip bombs are now your problem. Reject names containing .., and cap uncompressed size before piping anything onward.

actions/checkout 5 → 6 is CI-only. The other 1.4.0 change is a Dependabot bump and has zero runtime impact — don't waste review time on it.

Upgrade path is trivial. 1.3 → 1.4 is additive (one new factory); no deprecations, no signature changes. Bump the constraint and ship.

No config file, no service provider. If you're looking for config/resource-stream.php or a publishable provider, there isn't one. Any tuning (chunk sizes, temp directories) lives wherever you call stream_copy_to_stream, not in package config.

Extension points. If you need gzip, S3-tailing, or a database BLOB source, implement the package's factory/stream interface rather than branching inside your controllers. That keeps every source interchangeable at the call site and keeps the writeStream/streamDownload glue identical across features.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings