keven/append-stream
A Laravel/PHP package that provides a simple AppendStream abstraction for writing data to append-only streams, with a straightforward API for appending and managing stream contents.
This is a micro-package (3 stars, no dependents), so treat the source as the documentation. Before writing any integration code, open the repository and read the class in src/ end to end — it is almost certainly a few dozen lines. You need to answer three questions from that read:
stream_copy_to_stream() everything into a php://temp buffer up front? This single answer determines whether the package is usable for large files or only small ones.AppendStream::make([...]), ::create()) or an instance you call ->append($resource) on repeatedly, then read via fread()/stream_get_contents() or hand to a response.resource or a PSR-7 StreamInterface. That changes how you wire it into Laravel.Install and smoke-test in Tinker before touching a controller:
composer require keven/append-stream
php artisan tinker
$a = fopen(storage_path('app/part1.txt'), 'rb');
$b = fopen(storage_path('app/part2.txt'), 'rb');
$combined = /* whatever the package's constructor is */;
echo stream_get_contents($combined); // verify order and boundaries
First real use case: concatenating sequentially-produced chunks of the same logical file — HLS .ts segments, chunked uploads, paginated API payloads dumped to disk, or generated CSV fragments — and shipping them as one download without shelling out to cat or ffmpeg.
Chunked-upload assembly. If your app writes upload chunks to disk and you want to stream the reassembled file back without a second write pass, build the combined stream in a controller and hand it to streamDownload:
return response()->streamDownload(function () use ($chunks) {
$resources = array_map(fn ($p) => fopen($p, 'rb'), $chunks);
$stream = AppendStream::make($resources); // verify name
while (! feof($stream)) {
echo fread($stream, 8192);
flush();
}
}, 'video.mp4');
HLS / media segment serving. Same shape, but set Content-Type: video/mp2t and skip Content-Length (see gotchas). This is the package's most natural fit because segment counts are small and sources are files on disk.
Multi-source S3 / storage reads. Open each object via Storage::readStream($key) and append in order. Only do this if the package is lazy; otherwise you are buffering the whole object set in memory and you should use League\Flysystem's own concatenation or a queue job instead.
Composing generated payloads. When you generate N JSON lines or CSV rows in separate temp streams and want a single response, append them and let the framework stream it out. Useful in report exports where each section comes from a different query.
Wrap it behind your own service. Given the package's size and bus-factor of essentially one, put a thin ConcatenatedStreamFactory in app/Support/ and call the package from exactly one place. If the package dies or you need to fork it, you edit one file. The fallback implementation is roughly:
$out = fopen('php://temp', 'w+b');
foreach ($resources as $r) { stream_copy_to_stream($r, $out); }
rewind($out);
Memory is the whole ballgame. If the implementation copies into php://temp, note that php://temp spills to disk past 2 MB by default but still costs you a full write pass and double disk I/O. For anything over a few hundred MB, that defeats the purpose of streaming. Check the source; if it's eager, either keep input sizes small or write the lazy wrapper yourself.
You usually cannot set Content-Length. Unless every source is a seekable file where you can sum filesize(), the total size is unknown when headers go out. Omit the header and let the response be chunked, or compute the sum up front from filesystem metadata. Do not guess — a wrong Content-Length truncates the download silently.
Check close semantics. Does the combined stream close the underlying resources when it is destroyed or when fclose is called? If not, you leak file handles in workers. If it does, do not also fclose the sources yourself — double-close is a warning and undefined behaviour on some builds.
Non-seekable sources will break rewinds. If any input is a socket, a pipe, or an already-consumed stream, fseek/rewind on the combined stream may throw or silently return partial data. This matters because Laravel's streamDownload and Symfony's StreamedResponse can call the callback more than once in some test setups.
Errors mid-stream are unfixable. Once bytes are on the wire you cannot change the status code or swap to an error page. Validate every source (exists, readable, non-empty) before returning the streamed response, and log failures in the read loop rather than throwing.
Empty and zero-byte streams. An empty input resource is not necessarily an error but can produce surprises — some implementations loop forever when a source immediately returns '' without feof(). Filter empties or add a guard.
Testing. Stream resources are not serializable, so avoid putting them in queued jobs or cached values. In tests, build sources with fopen('php://memory', 'r+'), write, rewind(), and assert on stream_get_contents($combined).
Low maintenance risk. With no dependents and a tiny surface area, do not plan on upstream fixes. Vendor the code or pin the exact commit in composer.json, and read the diff before any composer update. The extension point is not the package — it's your own wrapper around it.
How can I help you explore Laravel packages today?