zbateson/stream-decorators
Lightweight PHP stream decorators for composing and filtering streams. Wrap any resource to add behaviors like buffering, limiting, logging, or on-the-fly transforms while keeping a standard stream interface. Useful for email parsing, I/O pipelines, and testing.
Install via Composer:
composer require zbateson/stream-decorators
First Use Case: Decode a base64-encoded email attachment stream while preserving original charset:
use ZBateson\StreamDecorators\{Base64Stream, CharsetStream};
use GuzzleHttp\Psr7\Utils;
// Convert a file stream to a PSR-7 stream
$fileStream = Utils::streamFor(fopen('attachment.txt', 'r'));
// Decode base64 and convert from ISO-8859-1 to UTF-8
$processedStream = new CharsetStream(
new Base64Stream($fileStream),
'ISO-8859-1',
'UTF-8'
);
// Read the decoded content
$content = Utils::streamFor($processedStream)->getContents();
Where to Look First:
src/ directory for decorator classesREADME.md for constructor signatures and usage examplestests/ for edge-case handling patterns// Decode → Charset → Chunk → Log
$stream = new LogStream(
new ChunkSplitStream(
new CharsetStream(
new Base64Stream($rawStream),
'UTF-16',
'UTF-8'
),
76 // RFC 2045 line length
),
'email_processor'
);
// Rate-limit response streams
public function streamResponse($stream, $maxBytes = 1024)
{
return new Response(
new SeekingLimitStream($stream, $maxBytes),
200,
['Content-Type' => 'application/octet-stream']
);
}
// Process stdin with charset conversion
$stdin = new CharsetStream(
new NonClosingStream(fopen('php://stdin', 'r')),
'Windows-1252',
'UTF-8'
);
// Read line-by-line without closing stdin
while (($line = Utils::readLine($stdin)) !== false) {
// Process line
}
public function testCharsetConversion()
{
$mockStream = new class implements StreamInterface { ... };
$converted = new CharsetStream($mockStream, 'UTF-8', 'ASCII');
$this->assertEquals('Hello', Utils::streamFor($converted)->getContents());
}
// Factory for configurable stream pipelines
public function createStreamPipeline(array $config, StreamInterface $stream)
{
$decorators = [
'Base64Stream' => Base64Stream::class,
'CharsetStream' => CharsetStream::class,
// ...
];
$pipeline = $stream;
foreach ($config['decorators'] as $decorator => $args) {
$pipeline = new $decorators[$decorator]($pipeline, ...$args);
}
return $pipeline;
}
CharsetStream Quirks:
read(). Avoid fread()—use Utils::readLine() or getContents().mb_detect_encoding().Non-Seekable Streams:
SeekingLimitStream fails silently on non-seekable streams (e.g., php://input).LimitStream instead or add seekability checks:
if (!$stream->isSeekable()) {
throw new RuntimeException('Stream must be seekable');
}
Resource Leaks:
NonClosingStream prevents close()/detach(), but the underlying stream may still leak if not manually closed.DecoratedCachingStream for temporary caching:
$cached = new DecoratedCachingStream(new NonClosingStream($stream));
BufferStream Memory Bloat:
BufferStream loads entire content into memory. Avoid for large files (>10MB).SeekingLimitStream with chunked reads instead.PregReplaceFilterStream Edge Cases:
$1) in replacements may cause infinite loops.if (!preg_match('/pattern/', '')) {
throw new InvalidArgumentException('Invalid regex');
}
Stream Position Issues:
// Log stream position after each operation
$stream->on('read', function ($bytesRead) {
error_log("Position: {$stream->tell()}, Bytes read: {$bytesRead}");
});
Charset Detection:
$detected = mb_detect_encoding($stream->getContents(), ['UTF-8', 'ISO-8859-1'], true);
Decorator Stack Visualization:
$stream->debug(function ($method, $args) {
error_log("Calling {$method} on " . get_class($stream));
});
Custom Decorators:
Extend StreamDecoratorTrait for reusable logic:
class CompressionStream extends StreamDecoratorTrait
{
public function __construct(StreamInterface $stream, $level = 6)
{
$this->stream = $stream;
$this->level = $level;
}
public function read($length)
{
$data = $this->stream->read($length);
return gzdecode($data, $this->level);
}
public function write($data)
{
return $this->stream->write(gzencode($data, $this->level));
}
}
Event-Driven Decorators:
Use StreamInterface events (if supported) or wrap with a proxy:
class LoggingStream implements StreamInterface
{
private $stream;
private $logger;
public function __construct(StreamInterface $stream, LoggerInterface $logger)
{
$this->stream = $stream;
$this->logger = $logger;
}
public function read($length)
{
$this->logger->info('Reading from stream');
return $this->stream->read($length);
}
// Delegate other methods...
}
Laravel Service Provider Integration:
public function register()
{
$this->app->bind(StreamInterface::class, function ($app) {
return new CharsetStream(
new Base64Stream(
fopen('php://temp', 'r+')
),
'UTF-16',
'UTF-8'
);
});
}
CharsetStream Performance:
$cached = new DecoratedCachingStream($charsetStream);
Base64/Quoted-Printable:
\n) are preserved in output. For RFC 2045 compliance, wrap with ChunkSplitStream:
new ChunkSplitStream(new Base64Stream($stream), 76);
SeekingLimitStream:
$stream->seek(100);
$data = $stream->read(50); // Reads bytes 100-149
How can I help you explore Laravel packages today?