Skip to content

SseStream

A write-once PSR-7 stream backed by an iterable of SseEvent (or plain string) items, typically a generator produced by an SseStreamingAction::streamEvents() implementation.

There are three ways to drain it, and only one may be used per instance: - writeTo(), a push loop that hands each formatted event to a sink and stops early when the sink reports the client is gone. SapiEmitter uses this. - read()/eof(), an incremental pull for consumers whose only streaming API is chunk-at-a-time (the RoadRunner responder). - __toString()/getContents(), which buffers everything in one pass, for anything treating the body as an ordinary string (dev-exception rendering, HttpTestCase assertions).

Mixing them throws rather than silently dropping events, since the backing iterable can only be traversed once.

final class SseStream implements StreamInterface

ImplementsStreamInterface
SourceHttp/Sse/SseStream.php

public function __construct(iterable<SseEvent|string> $events): mixed

ParameterTypeDescription
$eventsiterable``<SseEvent`string>`

Returns mixed

MethodDescription
__toString(): stringReads all data from the stream into a string, from the beginning to end.
close(): voidMarks the stream consumed so no further draining is attempted; there is no underlying resource to release.
detach(): mixedMarks the stream consumed and returns null, since the body is an iterable rather than a resource.
eof(): boolReports whether there is anything left to emit.
getContents(): stringDrains the whole event iterable in one pass and returns the formatted bytes.
getMetadata(mixed $key = null): mixedReports no stream metadata: an empty array for a whole-set request, null for any single key.
getSize(): ?intAlways returns null: the byte length of a generator-backed event stream is not known in advance.
isReadable(): boolAlways true; whether a given read() succeeds still depends on the stream not having been drained another way.
isSeekable(): boolAlways false: events are produced once, in order, and cannot be revisited.
isWritable(): boolAlways false: content comes from the iterable given to the constructor, never from callers.
read(mixed $length): stringPulls events on demand, returning at most $length bytes and keeping the remainder for the next call.
rewind(): voidTolerated as a no-op while nothing has been consumed yet, because that is how PSR-7 consumers conventionally open a body they are about to read (RoadRunner’s chunked responder does exactly this).
seek(mixed $offset, mixed $whence = SEEK_SET): voidNever moves a position.
tell(): intNever returns a position.
write(mixed $string): intNever accepts a write.
writeTo(callable $sink): voidDrains the event iterable, formatting each item and passing it to $sink.

public function __toString(): string

Reads all data from the stream into a string, from the beginning to end.

This method MUST attempt to seek to the beginning of the stream before reading data and read the stream until the end is reached.

Warning: This could attempt to load a large amount of data into memory.

This method MUST NOT raise an exception in order to conform with PHP’s string casting operations.

Returns string

public function close(): void

Marks the stream consumed so no further draining is attempted; there is no underlying resource to release.

public function detach(): mixed

Marks the stream consumed and returns null, since the body is an iterable rather than a resource.

Returns mixed

public function eof(): bool

Reports whether there is anything left to emit.

On the incremental read() path this is true only once the iterable is exhausted and the pending buffer has been handed out; otherwise it reflects whether the stream has been drained, closed or detached.

Returns bool

public function getContents(): string

Drains the whole event iterable in one pass and returns the formatted bytes.

This is the buffering path: nothing is streamed, and the stream is left consumed.

Returns string

ThrowsWhen
RuntimeExceptionwhen the incremental read() path has already been started

public function getMetadata(mixed $key = null): mixed

Reports no stream metadata: an empty array for a whole-set request, null for any single key.

There is no PHP stream resource behind this body, so there is nothing to describe.

ParameterTypeDescription
$keymixed

Returns mixed

public function getSize(): ?int

Always returns null: the byte length of a generator-backed event stream is not known in advance.

Returns ?``int

public function isReadable(): bool

Always true; whether a given read() succeeds still depends on the stream not having been drained another way.

Returns bool

public function isSeekable(): bool

Always false: events are produced once, in order, and cannot be revisited.

Returns bool

public function isWritable(): bool

Always false: content comes from the iterable given to the constructor, never from callers.

Returns bool

public function read(mixed $length): string

Pulls events on demand, returning at most $length bytes and keeping the remainder for the next call.

Blocks in the underlying generator exactly as long as the next event takes to produce, so a consumer that reads in a loop streams rather than buffers — which is what makes SSE work on a runtime whose only streaming API is “give me a chunk at a time” (RoadRunner) rather than a write callback (the SAPI emitter, which uses writeTo() instead).

Mutually exclusive with writeTo()/getContents(): an iterable can only be drained once, so mixing the two throws rather than silently losing events.

ParameterTypeDescription
$lengthmixed

Returns string

public function rewind(): void

Tolerated as a no-op while nothing has been consumed yet, because that is how PSR-7 consumers conventionally open a body they are about to read (RoadRunner’s chunked responder does exactly this).

Once events have started flowing there is nothing to rewind to.

public function seek(mixed $offset, mixed $whence = SEEK_SET): void

Never moves a position.

ParameterTypeDescription
$offsetmixed
$whencemixed
ThrowsWhen
RuntimeExceptionalways, as the stream is not seekable

public function tell(): int

Never returns a position.

Returns int

ThrowsWhen
RuntimeExceptionalways, as the stream has no byte offset to report

public function write(mixed $string): int

Never accepts a write.

ParameterTypeDescription
$stringmixed

Returns int

ThrowsWhen
RuntimeExceptionalways; produce events through the constructor’s iterable instead

public function writeTo(callable $sink): void

Drains the event iterable, formatting each item and passing it to $sink.

Stops early if $sink returns false (e.g. the client disconnected mid-stream).

ParameterTypeDescription
$sinkcallable