guzzlehttp/streams
A lightweight PHP library for working with streams, offering object-oriented wrappers, lazy and buffered streams, filtering, limiting, and utility helpers. Once used by Guzzle to power HTTP request and response bodies, it remains useful for composing and manipulating stream data.
First: confirm you actually have this package. guzzlehttp/streams is archived and is not something you add to a new Laravel app. It ships only as a legacy transitive dependency — effectively guzzlehttp/guzzle: ~5.0, which Laravel 5.x-era code and a handful of old SDKs pinned. Check with:
composer show guzzlehttp/streams # present at all?
composer why guzzlehttp/streams # who is dragging it in
composer show guzzlehttp/guzzle | grep versions
If your app runs Guzzle 6/7 (everything Laravel 6+ uses, including Illuminate\Support\Facades\Http), the same abstractions live in guzzlehttp/psr7 — see the mapping table below and use that instead. Everything after this paragraph applies only to code that genuinely still resolves GuzzleHttp\Stream\*.
Where to look first. The whole API is three things:
GuzzleHttp\Stream\Stream — the resource wrapper; Stream::factory($resource) is the entry point for 90% of usage.GuzzleHttp\Stream\StreamInterface — the contract (read, write, seek, tell, eof, getContents, getSize, isSeekable, detach, getMetadata, __toString).GuzzleHttp\Stream\Utils — static helpers, chiefly Utils::copyToStream($src, $dst, $maxLen = -1) and Utils::tryFopen($uri, $mode).First use case. You have a raw PHP resource from somewhere Laravel-ish — Storage::disk('s3')->readStream($path), fopen('php://temp','w+'), a proc_open pipe — and some downstream API wants a uniform "readable thing" with eof() and getContents() semantics. Wrap it:
use GuzzleHttp\Stream\Stream;
$resource = Storage::disk('s3')->readStream('exports/customers.csv');
$stream = Stream::factory($resource);
while (! $stream->eof()) {
$chunk = $stream->read(8192);
// ...
}
That is the entire value proposition. If you are not doing exactly this, you probably don't need the package.
Compose rather than buffer. The decorator streams are the reason the package exists at all. In a Laravel job or console command:
LimitStream — send only the first N bytes of a file to an API; wrap once, don't slice strings.AppendStream — concatenate a header, a CSV body and a footer into one upload body without a temp file.CachingStream — makes a non-seekable source (S3 body, HTTP response, pipe) rewindable by spooling to php://temp. This is the one you will reach for most, because retries and payload signing both require rewinding.NoSeekStream — the inverse: wrap a stream you hand to untrusted code so it can't rewind and replay it.PumpStream, BufferStream, LazyOpenStream, DroppingStream — lazily-populated data, in-memory buffers, open-on-first-read files, and "skip the first N bytes" respectively.use GuzzleHttp\Stream\CachingStream;
use GuzzleHttp\Stream\LimitStream;
use GuzzleHttp\Stream\Stream;
$raw = Storage::disk('s3')->readStream('reports/big.pdf');
$cached = new CachingStream(Stream::factory($raw)); // now seekable/rewindable
$cached->seek(0); // safe before a retry
$head = new LimitStream($cached, 1024, 0); // first 1 KiB, for sniffing
Streaming out of Laravel, not through it. The failure mode is $stream->getContents() into an Eloquent accessor and OOM-ing the worker. The correct exit shape is Laravel's own streaming response, which never holds the body:
return response()->streamDownload(function () use ($stream) {
$out = fopen('php://output', 'w');
\GuzzleHttp\Stream\Utils::copyToStream($stream, \GuzzleHttp\Stream\Stream::factory($out));
fclose($out);
$stream->close();
}, 'export.csv', ['Content-Type' => 'text/csv']);
Integration seams to know. Laravel's filesystem layer gives you resources, not streams — readStream(), Storage::path(), php://temp all need Stream::factory() before the package is useful. Conversely, anything that wants a PSR-7 body (Http::withBody(), Response::setContent(), Symfony's StreamedResponse in newer versions) will not accept this package's StreamInterface; you must bridge via getContents() or getMetadata('uri').
Testing. In Storage::fake('local') tests, assert on the resource, not the stream: $this->assertIsResource($fake->readStream(...)). When you must compare stream contents, rewind() first — the pointer is part of the object's state and PHPUnit's assertion failures will silently leave it advanced.
Migration path (the real long-term pattern). If you touch GuzzleHttp\Stream\*, port it in the same PR — the classes are one-for-one:
Legacy (GuzzleHttp\Stream) |
Current (GuzzleHttp\Psr7) |
|---|---|
Stream::factory($r) |
Utils::streamFor($r) |
Utils::copyToStream() |
Utils::copyToStream() |
LimitStream |
LimitStream |
CachingStream |
CachingStream |
AppendStream |
AppendStream |
LazyOpenStream |
LazyOpenStream |
BufferStream |
BufferStream |
PumpStream |
PumpStream |
NoSeekStream |
NoSeekStream |
DroppingStream |
DroppingStream |
Constructor signatures are near-identical; psr7 additionally returns PSR-7 StreamInterface, so every Laravel/Guzzle type hint starts working again.
GuzzleHttp\Stream\StreamInterface is a different interface from Psr\Http\Message\StreamInterface, despite identical method names. Passing one where the other is type-hinted yields a fatal TypeError that reads like a version mismatch and isn't. Grep your use statements before debugging anything else.composer audit and check whether an old guzzlehttp/guzzle is pinning you.getContents() drains the stream and is not idempotent. Neither is __toString() (which internally rewinds if seekable and then drains). Calling either twice gives you '' the second time. Always rewind() between reads, and only on streams where isSeekable() is true.getSize() returns null for unknown-length streams — pipes, chunked HTTP bodies, php://input. Any code doing intdiv($stream->getSize(), $chunk) will fatal on null. Fall back to a loop over eof() and never trust the size for allocation.CachingStream before you enter the retry path.detach() transfers ownership; close() afterwards does nothing. If you detach() and never fclose() the resource, you leak file handles in long-running queue workers. Prefer close() and let the wrapper clean up.Stream classes and a use statement that resolves to whichever one you didn't want. When a Stream method mysteriously doesn't exist, get_class($stream) is the fastest diagnostic.Stream::factory() accepts an options array (size, seekable, readable, writable, and a metadata array). If you have a resource wrapping an unusual URI scheme, overriding size avoids the fstat() round-trip and the null-size trap above — but you own the correctness of that number.Utils::tryFopen() swallows the fopen warning and throws a RuntimeException with the underlying message. It is the safer call in a web request path, where a PHP warning becomes a 500 with a leaked filesystem path.Stream writes the metadata array and loses the resource; the worker gets a closed or empty stream. Pass the path or the storage key and re-open inside handle().How can I help you explore Laravel packages today?