Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .horde.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ license:
uri: http://www.horde.org/licenses/lgpl21
dependencies:
required:
php: ^7.4 || ^8
php: ^8.1
composer:
horde/exception: ^3
dev:
Expand All @@ -40,4 +40,8 @@ dependencies:
horde/util: ^3
keywords:
- streams
- bytestream
vendor: horde
quality:
phpstan:
level: 5
117 changes: 117 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Horde\Stream

A managed stream abstraction for PHP that provides type guarantees,
buffering strategies, lifecycle management and protocol-oriented
read operations. Part of the [Horde](https://www.horde.org/) project.

## Why not PSR-7 StreamInterface?

PSR-7 streams (`Psr\Http\Message\StreamInterface`) model HTTP message bodies:
a thin wrapper around a PHP resource with `read()`, `write()` and `seek()`.
They are the right tool for HTTP request/response transport.

Horde\Stream targets a different problem space: **parsing and constructing
structured data over byte streams**, the kind of work that IMAP, SMTP, MIME,
and ManageSieve libraries do on every connection. This requires operations
that PSR-7 does not offer:

- **Delimiter scanning** - read until `}`, `\r\n` or any multi-byte
delimiter without buffering the entire stream into a string first.
- **Positional search** - find the byte offset of a substring, forward or
reverse, without consuming data.
- **Peek without consume** - inspect upcoming bytes and rewind automatically.
- **UTF-8 character-level seeking and reading** - navigate by character count
rather than byte count for multibyte-safe protocol handling.
- **EOL auto-detection** - determine whether a stream uses LF or CRLF line
endings.
- **Polymorphic write** - `add()` accepts a string, a PHP resource or
another `StreamInterface`, copying data efficiently in chunks.
- **Automatic memory management** - `TempString` starts in a pure-PHP string
backend and transparently spills to `php://temp` when size exceeds a
configurable threshold.
- **Safe cloning and serialization** - clone a stream to get an independent
copy; serialize to persist both data and cursor position.

A future PSR-7 adapter layer can bridge the two worlds when HTTP interop is
needed. See [doc/USAGE.md](doc/USAGE.md) for details and examples.

## Installation

```bash
composer require horde/stream
```

For the string-backed stream (`StringStream`), also install the stream wrapper:

```bash
composer require horde/stream_wrapper
```

## Quick start

```php
use Horde\Stream\Temp;

$stream = new Temp();
$stream->add("From: alice@example.com\r\nSubject: Hello\r\n\r\nBody text.");
$stream->rewind();

// Read header lines
while (!$stream->eof()) {
$line = $stream->getToChar("\r\n", all: false);
if ($line === '') {
break; // blank line = end of headers
}
echo $line, "\n";
}

// Read remaining body
echo $stream->substring(), "\n";
```

## Stream implementations

| Class | Backend | Use case |
|-------|---------|----------|
| `Temp` | `php://temp` | General-purpose buffering. Optional `maxMemory` parameter controls spill-to-disk threshold. |
| `Existing` | Caller-provided resource | Wrap a socket, file handle or any open PHP stream resource. |
| `StringStream` | `horde/stream_wrapper` | String-backed stream with full seek support. Requires `horde/stream_wrapper`. |
| `TempString` | String, spills to `php://temp` | Starts in fast string backend, transparently switches to temp stream when data exceeds `maxMemory`. |

## Interface hierarchy

```
Readable Writable Seekable Stringable
\ | / /
\ | / /
StreamInterface ----------------
|
AbstractStream
/ | \ \
Temp Existing StringStream TempString
```

`StreamInterface` composes `Readable`, `Writable`, `Seekable` and
`Stringable`. Consumer code can type-hint on the narrow interface when only
a subset of capabilities is needed.

## Documentation

- [doc/USAGE.md](doc/USAGE.md) - API reference, patterns and protocol
examples.
- [doc/UPGRADING.md](doc/UPGRADING.md) - Migration guide from `Horde_Stream`
(lib/) to `Horde\Stream` (src/).

## Testing

```bash
# Unit tests (default suite)
phpunit

# Integration tests (requires horde/stream_wrapper)
phpunit --testsuite integration
```

## License

LGPL 2.1. See [LICENSE](LICENSE) for details.
8 changes: 6 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"autoload": {
"psr-0": {
"Horde_Stream": "lib/"
},
"psr-4": {
"Horde\\Stream\\": "src/"
}
},
"autoload-dev": {
Expand All @@ -45,5 +48,6 @@
"branch-alias": {
"dev-FRAMEWORK_6_0": "2.x-dev"
}
}
}
},
"minimum-stability": "dev"
}
228 changes: 228 additions & 0 deletions doc/UPGRADING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
# Upgrading from Horde_Stream to Horde\Stream

This guide covers the migration from the legacy `Horde_Stream` classes
(`lib/` PSR-0) to the modern `Horde\Stream` namespace (`src/` PSR-4).

Both namespaces coexist in the same package. No code is removed. You can
migrate at your own pace.

## Class mapping

| Legacy (lib/) | Modern (src/) |
|----------------|---------------|
| `Horde_Stream` | `Horde\Stream\AbstractStream` (not instantiated directly) |
| `Horde_Stream_Temp` | `Horde\Stream\Temp` |
| `Horde_Stream_Existing` | `Horde\Stream\Existing` |
| `Horde_Stream_TempString` | `Horde\Stream\TempString` |
| `Horde_Stream_Wrapper_String` | `Horde\Stream\StringStream` (wraps the wrapper internally) |
| `Horde_Stream_Exception` | `Horde\Stream\StreamException` |
| *(none)* | `Horde\Stream\StreamInterface` |
| *(none)* | `Horde\Stream\Readable` |
| *(none)* | `Horde\Stream\Writable` |
| *(none)* | `Horde\Stream\Seekable` |

## Constructor changes

Legacy classes accepted an `$opts` array. Modern classes use typed named
parameters.

### Horde_Stream_Temp

```php
// Legacy
$stream = new Horde_Stream_Temp(['max_memory' => 2097152]);

// Modern
$stream = new Horde\Stream\Temp(maxMemory: 2097152);
```

### Horde_Stream_Existing

```php
// Legacy
$stream = new Horde_Stream_Existing(['stream' => $resource]);

// Modern
$stream = new Horde\Stream\Existing($resource);
```

### Horde_Stream_TempString

```php
// Legacy
$stream = new Horde_Stream_TempString(['max_memory' => 1048576]);

// Modern
$stream = new Horde\Stream\TempString(maxMemory: 1048576);
```

## Property changes

### utf8_char -> utf8Char

The magic property `utf8_char` is now a typed public property.

```php
// Legacy
$stream->utf8_char = true;

// Modern
$stream->utf8Char = true;
```

### stream -> getResource()

The public `$stream` property is replaced by a method.

```php
// Legacy
$resource = $stream->stream;

// Modern
$resource = $stream->getResource();
```

## Method signature changes

### Return types

Methods that returned `bool` for success/failure now return `void` and throw
`StreamException` on failure.

| Method | Legacy return | Modern return |
|--------|-------------|---------------|
| `rewind()` | `bool` | `void` (throws on failure) |
| `seek()` | `bool` | `void` (throws on failure) |
| `end()` | `bool` | `void` (throws on failure) |
| `pos()` | `int\|false` | `int` (throws on failure) |

### seek()

The second parameter changed from positional `$curr` to named `$fromCurrent`.
Behavior is identical.

```php
// Legacy - seek to absolute position 5
$stream->seek(5, false);

// Modern - identical call, but the parameter is named
$stream->seek(5, fromCurrent: false);
```

The optional third parameter for UTF-8 character-mode seeking changed from
positional to named:

```php
// Legacy
$stream->seek(2, false, true);

// Modern
$stream->seek(2, fromCurrent: false, char: true);
```

### add()

The second parameter changed from positional `$reset` to named. The `$data`
parameter now accepts `StreamInterface` in addition to `Horde_Stream`.

```php
// Legacy
$stream->add($data, true);

// Modern
$stream->add($data, reset: true);
```

### getToChar()

The second parameter changed from positional `$all` to named:

```php
// Legacy - don't strip repeated delimiters
$stream->getToChar("\n", false);

// Modern
$stream->getToChar("\n", all: false);
```

### substring()

The third parameter for UTF-8 character mode changed from positional to named:

```php
// Legacy
$stream->substring(0, 3, true);

// Modern
$stream->substring(0, 3, char: true);
```

### length()

The parameter for UTF-8 character counting changed from positional to named:

```php
// Legacy
$stream->length(true);

// Modern
$stream->length(utf8: true);
```

### search()

Parameters changed from positional to named:

```php
// Legacy
$stream->search('X', false, false);

// Modern
$stream->search('X', reverse: false, reset: false);
```

## Type-hinting

Legacy code that type-hints on `Horde_Stream` should be updated to use
`Horde\Stream\StreamInterface` or the narrower capability interfaces when
appropriate:

```php
// Legacy
function parse(Horde_Stream $stream): void { ... }

// Modern - full stream
function parse(StreamInterface $stream): void { ... }

// Modern - only needs read + position
function parse(Readable&Seekable $stream): void { ... }
```

The `add()` method on modern streams accepts both `StreamInterface` and
`Horde_Stream`, so you can pass legacy stream objects to modern code during
migration.

## Serialization

Legacy streams implemented `Serializable`. Modern streams use
`__serialize()` / `__unserialize()`. Both serialize data content and cursor
position. The serialized formats are not cross-compatible - you cannot
unserialize a legacy stream into a modern one or vice versa.

## Exceptions

Modern code throws `Horde\Stream\StreamException` (extends `RuntimeException`)
instead of `Horde_Stream_Exception`. Expected exhaustion (`getChar()` at EOF)
returns `false`, not an exception. Only actual failures throw:

- Failed seek, rewind or position query
- Invalid or truncated UTF-8 byte sequence
- Failed resource creation in constructors

## Removed features

- **Magic `__get` / `__set`**: The `utf8_char` magic property is replaced by
`public bool $utf8Char`. No other magic properties exist.
- **`Serializable` interface**: Replaced by `__serialize()` /
`__unserialize()`.
- **Array constructor options**: Replaced by typed named parameters.
Loading