stream-interop / interface
Interoperable interfaces for streams in PHP 8.4 and later.
Requires
- php: >=8.4
Requires (Dev)
- pds/composer-script-names: ^1.0
- pds/skeleton: ^1.0
- phpstan/phpstan: ^2.0
This package is auto-updated.
Last update: 2025-04-18 16:38:17 UTC
README
Stream-Interop publishes a standard set of interoperable interfaces providing a more object-oriented approach to encapsulating and interacting with stream resources in PHP 8.4+. It reflects, refines, and reconciles the common practices identified within several pre-existing projects.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174).
This package attempts to adhere to the Package Development Standards approach to naming and versioning.
Interfaces
Stream-Interop defines separate interfaces for various affordances around stream resources so that (1) implementations can advertise well-tailored affordances, and (2) consumers can typehint to the specific affordances they require for specific situations:
- Stream is a common baseline for streams.
- ResourceStream affords direct access to the encapsulated resource.
- ClosableStream affords closing the stream.
- SizableStream affords getting the full length of the stream in bytes.
- ReadableStream affords non-idempotent reading from the stream.
- SeekableStream affords moving the stream pointer.
- StringableStream affords idempotent reading from the stream.
- WritableStream affords writing to the stream at the current pointer position.
Stream-Interop also defines these marker interfaces:
- ReadonlyStream marks the stream as enforcing readonly constraints on the encapsulated resource.
- ImmutableStream marks the stream as enforcing immutability constraints on the encapsulated resource.
- StreamThrowable marks an Exception as stream-related.
Finally, Stream-Interop defines an interface of StreamTypeAliases to aid static analysis with PHPStan.
Stream
The Stream interface defines these properties common to all streams:
public metadata_array $metadata { get; }
- Represents the metadata for the encapsulated resource as if by
stream_get_meta_data()
. - The implementation MUST provide the most-recent metadata for the encapsulated resource at the moment of property access; if the encapsulated resource is closed, the implementation MUST return an empty array.
- The implementation MUST NOT allow
$metadata
to be publicly settable, either as a property or via property hook or method.
- Represents the metadata for the encapsulated resource as if by
It also defines these methods common to all streams:
-
public function isClosed() : bool
- Returns
true
if the encapsulated resource has been closed, orfalse
if not.
- Returns
-
public function isOpen() : bool
- Returns
true
if the encapsulated resource is still open, orfalse
if not.
- Returns
Notes:
-
The
$metadata
property is expected change dynamically. That is, as the encapsulated resource gets read from and written to, the metadata for that resource is likely to change. Thus, the$metadata
property value is expected to change along with it. In practical terms, this likely means astream_get_meta_data()
call on each access of$metadata
. -
There are no
isReadable()
, etc. methods. If necessary, such functionality can be determined by typehinting against the interface, or by checkinginstanceof
, etc. -
The encapsulated resource is not exposed publicly here. The encapsulated resource MAY remain private or protected. See the ResourceStream interface below for details on making the encapsulated resource publicly accessible.
ResourceStream
The ResourceStream interface extends Stream to define a property to allow public access to the encapsulated resource:
public resource $resource { get; }
- Represents the resource as if opened by
fopen()
,fsockopen()
,popen()
, etc. - The implementation MUST ensure
$resource
is aresource of type (stream)
; for example, as determined byget_resource_type()
. - The implementation SHOULD NOT allow
$resource
to be publicly settable, either as a property or via property hook or method.
- Represents the resource as if opened by
Notes:
-
Not all Stream implementations need to expose the encapsulated resource. Exposing the resource gives full control over it to consumers, who can then manipulate it however they like (e.g. close it, move the pointer, and so on). However, having access to the resource may be necessary for some consumers.
-
Some Stream implementations might not encapsulate a resource. Although a resource is the most common data source for a stream, other data sources MAY be used, in which cases ResourceStream implementation is neither appropriate nor necessary.
ClosableStream
The ClosableStream interface extends Stream to define this method:
public function close() : void
- Closes the encapsulated resource as if by
fclose()
,pclose()
, etc. - The implementation MUST throw a StreamThrowable on failure.
- Closes the encapsulated resource as if by
The implementation MAY close the encapsulated resource internally without affording ClosableStream.
Notes:
- Not all Stream implementations need to be closable. It may be important for resource closing to be handled by a separate service or authority, and not be closable by Stream consumers.
SizableStream
The SizableStream interface extends Stream to define this method:
public function getSize() : ?int<0,max>
- Returns the length of the encapsulated resource in bytes as if by the
fstat()
value forsize
, ornull
if indeterminate or on error.
- Returns the length of the encapsulated resource in bytes as if by the
The implementation MAY get the size of the encapsulated resource internally without affording SizableStream.
Notes:
- Not all Stream implementations need to be sizable. Some encapsulated resources may be unable to report a size; for example, remote or write-only resources.
ReadableStream
The ReadableStream interface extends Stream to afford these methods for non-idempotent reading from a resource:
-
public function eof() : bool
- Tests for end-of-file on the encapsulated resource as if by
feof()
. - The implementation MUST throw a StreamThrowable on failure.
- Tests for end-of-file on the encapsulated resource as if by
-
public function getContents() : string
- Returns the remaining contents of the resource from the current pointer position as if by
stream_get_contents()
. - The implementation MUST throw a StreamThrowable on failure.
- Returns the remaining contents of the resource from the current pointer position as if by
-
public function read(int<1,max> $length) : string
- Returns up to
$length
bytes from the encapsulated resource as if byfread()
. - The implementation MUST throw a StreamThrowable on failure.
- Returns up to
If the encapsulated resource is not readable at the time it becomes available to the ReadableStream, the implementation MUST throw a StreamThrowable .
The implementation MAY read from the encapsulated resource internally without affording ReadableStream.
Notes:
-
The
eof()
method is on ReadableStream, not Stream or SeekableStream. End-of-file is determined as a function of reading past the end of the file, not as of seeking to the end of the file. Cf. https://www.php.net/manual/en/function.feof.php#122925. -
These methods are non-idempotent. They may return different results on repeated sequential calls, and may have side effects (e.g., changing the position of the pointer.)
SeekableStream
The SeekableStream interface extends Stream to define methods for moving the stream pointer position back and forth:
-
public function rewind() : void
- Moves the stream pointer position to the beginning of the stream as if by
rewind()
. - The implementation MUST throw a StreamThrowable on failure.
- Moves the stream pointer position to the beginning of the stream as if by
-
public function seek(int $offset, int $whence = SEEK_SET) : void
- Moves the stream pointer position to the
$offset
as if byfseek()
. - The implementation MUST throw a StreamThrowable on failure.
- Moves the stream pointer position to the
-
public function tell() : int
- Returns the current stream pointer position as if by
ftell()
. - The implementation MUST throw a StreamThrowable on failure.
- Returns the current stream pointer position as if by
If the encapsulated resource is not seekable at the time it becomes available to the SeekableStream, the implementation MUST throw a StreamThrowable .
StringableStream
The StringableStream interface extends Stream to afford idempotent reading from the encaspulated resource:
-
public function __toString() : string
- Returns the entire contents of the encapsulated resource as if by
rewind()
ing before returningstream_get_contents()
. - After reading, The implementation MUST reposition the encapsulated resource pointer to its initial location.
- The implementation MUST throw a StreamThrowable on failure.
- Returns the entire contents of the encapsulated resource as if by
-
public function subString(int $offset, ?int $length) : string
- Returns a string from the encapsulated resource as if by
fseek()
ing before reading. - If the
$offset
is negative, the implementation MUST begin reading at that many bytes from the end of the stream; otherwise, the implementation MUST begin reading at that many bytes from the start of the stream. - If the
$length
is null, the implementation MUST return all remaining bytes from the stream; otherwise, the implementation MUST return up to that many bytes from the stream. - After reading, the implementation MUST reposition the encapsulated resource pointer to its initial location.
- The implementation MUST throw a StreamThrowable on failure.
- Returns a string from the encapsulated resource as if by
If the encapsulated resource is not readable at the time it becomes available to the StringableStream, the implementation MUST throw a StreamThrowable .
If the encapsulated resource is not seekable at the time it becomes available to the StringableStream, the implementation MUST throw a StreamThrowable .
The implementation MAY convert all or part of the encapsulated resource to a string internally without affording StringableStream.
Notes:
- These methods are idempotent. Repeated sequential calls to an unchanged resource will return the exact same result, without exposing side effects (such as the pointer position being changed).
WritableStream
The WritableStream interface extends Stream to define a single method for writing to a resource:
public function write(string|Stringable $data) : int
- Writes
$data
starting at the current stream pointer position, returning the number of bytes written, as if byfwrite()
. - The implementation MUST throw a StreamThrowable on failure.
- Writes
If the encapsulated resource is not writable at the time it becomes available to the WritableStream, the implementation MUST throw a StreamThrowable .
The implementation MAY write to the encapsulated resource internally without affording WritableStream.
ReadonlyStream
The ReadonlyStream marker interface indicates the implementation attempts to enforce these constraints:
-
The implementation MUST open the encapsulated resource inside the ReadonlyStream.
-
The implementation MUST open the encapsulated resource as
php://input
orphp://memory
. -
The implementation MAY open the encapsulated resource in a mode that allows writing (
rb+
,w+
, etc.) to allow initialization. -
The implementation MAY initialize the encapsulated resource after opening (e.g., by copying a constructor argument to the encapsulated resource).
-
The implementation MUST NOT modify, or allow modification of, the encapsulated resource content after initialization, whether by implementing WritableStream or by some other means.
-
The implementation MUST NOT expose the encapsulated resource, whether by implementing ResourceStream or by some other means.
-
The implementation MAY allow closing of the encapsulated resource, whether by implementing ClosableStream or by some other means.
Notes:
-
The readonly constraints are necessarily strict. Whereas readonly on scalar and array properties can be implemented relatively easily, readonly on a resource property is more difficult. The encapsulated resource, including both its content and its pointer, must be inaccessible from outside the ReadonlyStream to ensure it cannot be modified from outside the ReadonlyStream.
-
ReadonlyStream implementations may be memory-intensive. This is because they usually have to be initialized with a copy of the original resource, typically a file resource, thereby reading all of it into a
php://memory
resource. -
php://input
is natively readonly. It does not need to be copied to aphp://memory
resource, and does not need to be initialized.
ImmutableStream
The ImmutableStream marker interface extends ReadonlyStream to indicate the implementation attempts to enforce these constraints:
-
The implementation MUST adhere to all ReadonlyStream constraints.
-
The implementation MUST NOT allow non-idempotent reading of the encapsulated resource, whether by implementing ReadableStream or by some other means.
-
The implementation MUST NOT expose the state of the encapsulated resource pointer, whether by implementing SeekableStream or by some other means.
-
The implementation MUST NOT allow closing of the encapsulated resource before the ImmutableStream is destructed, whether by implementing ClosableStream or by some other means.
-
The implementation MUST NOT allow mutation of the
$metadata
property.
Notes:
- The immutability constraints are necessarily strict. Immutability of a resource is incompatible with non-idempotent reading; doing so modifies its pointer position, thereby changing its state. Likewise, closing the resource changes its state. These constraints leave only StringableStream and SizableStream as compatible interfaces.
StreamThrowable
The StreamThrowable interface extends Throwable to mark an Exception as stream-related. It adds no class members.
StreamTypeAliases
The StreamTypeAliases interface defines this custom PHPStan type to assist static analysis:
-
metadata_array
as if bystream_get_meta_data()
:array{ timed_out: bool, blocked: bool, eof: bool, unread_bytes: int, stream_type: string, wrapper_type: string, wrapper_data: mixed, mode: string, seekable: bool, uri?: string, mediatype?: string, base64?: bool }
-
stat_array
as if byfstat()
orstat()
:array{ dev:int<0,max>, ino:int<0,max>, mode:int<0,max>, nlink:int<0,max>, uid:int<0,max>, gid:int<0,max>, rdev:int<0,max>, size:int<0,max>, atime:int<0,max>, mtime:int<0,max>, ctime:int<0,max>, blksize:int<0,max>, blocks:int<0,max> }
Implementations
Implementations MAY encapsulate a string, or some other kind of data source, instead of a resource
.
Implementations encapsulating something besides a resource
MUST behave as if they encapsulate a resource.
Implementations advertised as readonly or immutable MUST be deeply readonly or immutable. With the exception of implementations meeting the specified ReadonlyStream or ImmutableStream conditions, they MUST NOT encapsulate any references, resources, mutable objects, objects or arrays encapsulating references or resources or mutable objects, and so on.
Implementations MAY define additional class members not defined in these interfaces; implementations advertised as readonly or immutable MUST make those additional class members deeply readonly or immutable.
Notes:
-
Reflection does not invalidate advertisements of readonly or immutable implementations. The ability of a consumer to use Reflection to mutate an implementation advertised as readonly or immutable does not constitute a failure to comply with Stream-Interop.
-
Reference implementations are avaliable at https://github.com/stream-interop/impl.
Q & A
What projects were used as reference points for Stream-Interop?
These are the reference projects for developing the above interfaces.
- amphp/byte-stream: https://github.com/amphp/byte-stream
- fzaninotto/streamer: https://github.com/fzaninotto/Streamer
- hoa/stream: https://github.com/hoaproject/Stream
- kraken-php/stream: https://github.com/kraken-php/stream
- psr/http-message: https://github.com/php-fig/http-message/blob/master/src/StreamInterface.php
- react/stream: https://packagist.org/packages/react/stream
- zenstruck/stream: https://github.com/zenstruck/stream
Please see README-RESEARCH.md for more information.
What about filters?
Stream filters are a powerful aspect of stream resources. However, as they operate on resources directly, creating interfaces for them is out-of-scope for Stream-Interop. Further, none of the projects included in the Stream-Interop research implemented filters, making it difficult to rationalize adding filter interfaces.
Even so, consumers are free to register filters on the resources they injection into a Stream. In addition, implementors are free to create filter mechanisms that intercept the input going into a WritableStream (e.g. via its write()
method) or the output coming from a ReadableStream (e.g. via its read()
method).
Why is there no Factory interface?
The sheer volume of possible combinations of the various interfaces makes it difficult to provide a factory with proper return typehints. Implementors are encouraged to develop their own factories with proper typehinting.