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

Stream Decorators Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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 classes
  • README.md for constructor signatures and usage examples
  • tests/ for edge-case handling patterns

Implementation Patterns

1. Email Processing Pipeline

// 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'
);

2. Laravel Request/Response Wrapping

// Rate-limit response streams
public function streamResponse($stream, $maxBytes = 1024)
{
    return new Response(
        new SeekingLimitStream($stream, $maxBytes),
        200,
        ['Content-Type' => 'application/octet-stream']
    );
}

3. CLI File Processing

// 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
}

4. Testing with Mock Streams

public function testCharsetConversion()
{
    $mockStream = new class implements StreamInterface { ... };
    $converted = new CharsetStream($mockStream, 'UTF-8', 'ASCII');

    $this->assertEquals('Hello', Utils::streamFor($converted)->getContents());
}

5. Dynamic Decorator Composition

// 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;
}

Gotchas and Tips

Pitfalls

  1. CharsetStream Quirks:

    • Multi-byte encodings (UTF-16/32) may return partial characters on read(). Avoid fread()—use Utils::readLine() or getContents().
    • Fix: Always specify the correct source/target charset. Test with mb_detect_encoding().
  2. Non-Seekable Streams:

    • SeekingLimitStream fails silently on non-seekable streams (e.g., php://input).
    • Fix: Use LimitStream instead or add seekability checks:
      if (!$stream->isSeekable()) {
          throw new RuntimeException('Stream must be seekable');
      }
      
  3. Resource Leaks:

    • NonClosingStream prevents close()/detach(), but the underlying stream may still leak if not manually closed.
    • Fix: Pair with DecoratedCachingStream for temporary caching:
      $cached = new DecoratedCachingStream(new NonClosingStream($stream));
      
  4. BufferStream Memory Bloat:

    • BufferStream loads entire content into memory. Avoid for large files (>10MB).
    • Fix: Use SeekingLimitStream with chunked reads instead.
  5. PregReplaceFilterStream Edge Cases:

    • Regex backreferences ($1) in replacements may cause infinite loops.
    • Fix: Validate regex patterns before use:
      if (!preg_match('/pattern/', '')) {
          throw new InvalidArgumentException('Invalid regex');
      }
      

Debugging Tips

  • 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));
    });
    

Extension Points

  1. 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));
        }
    }
    
  2. 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...
    }
    
  3. 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'
            );
        });
    }
    

Configuration Quirks

  • CharsetStream Performance:

    • UTF-16/32 conversions are ~3x slower than ASCII. Cache converted streams if reused:
      $cached = new DecoratedCachingStream($charsetStream);
      
  • Base64/Quoted-Printable:

    • Line endings (\n) are preserved in output. For RFC 2045 compliance, wrap with ChunkSplitStream:
      new ChunkSplitStream(new Base64Stream($stream), 76);
      
  • SeekingLimitStream:

    • Seeks before reading, not after. For partial reads, use:
      $stream->seek(100);
      $data = $stream->read(50); // Reads bytes 100-149
      
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.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table