feat(php): add SSE / NDJSON / text streaming support#15882
Conversation
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
SDK Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
Three follow-ups to the SSE streaming runtime to close the parity gap with the Python and Go SDK generators: - New SseStream::events() yields typed SseEvent<T> with WHATWG envelope fields (event, id, retry). lastEventId persists across events per spec; null bytes in id and non-integer retry values are rejected. - 1 MiB default maxBufferSize on every Stream wrapper, configurable per call. Guards line buffer and accumulated SSE event data against pathological streams; throws RuntimeException on overflow. - SseStream constructor validates Content-Type contains text/event-stream and rejects non-UTF-8 charset parameters per the WHATWG SSE spec. 13 new PHPUnit cases cover metadata exposure, lastEventId persistence, null-byte / non-integer field rejection, Content-Type / charset validation, missing-header tolerance, and buffer overflow.
Six follow-ups to the SSE streaming runtime, surfacing from a fresh review of Stream.Template.php for unidiomatic constructions: 1. Buffer cap during accumulation (real correctness gap): new appendWithinCap() helper throws BEFORE the concat allocates. A hostile stream sending many `data:` lines without a dispatching blank line used to grow $dataBuffer unbounded and only trip the check at dispatch — too late. Now bounded by the configured limit. 2. Split StreamFormat into its own template file for PSR-4 autoload correctness. Generated SDKs ship a separate StreamFormat.php. 3. Encapsulate $deserializer: private field behind a protected deserialize() method. SseStream::events() now calls $this->deserialize() instead of accessing the closure directly. 4. Strip a leading UTF-8 BOM per WHATWG §9.2.4. Handles BOM split across read chunks via a deferred check. 5. DEFAULT_MAX_BUFFER_SIZE = 1_048_576 (numeric separator). 6. Stream::__construct is now protected — external callers must use SseStream, JsonStream, or TextStream. Wrappers' parent::__construct calls are unaffected. Adds three new tests: cumulative-accumulation cap, BOM at start, BOM not stripped mid-stream.
…SDKs
Trim WHAT-narration and internal implementation rationale from comments
in Stream.Template.php and StreamTest.Template.php — these files are
emitted into every generated SDK and were exposing dev-side context
("a hostile stream cannot drive us past the limit and then trip the
check", "The constructor is protected to enforce this", etc.) that
customers don't need. WHATWG spec citations and non-obvious WHY
comments are kept.
- Collapse duplicated enumString switch arm into the primitive case in buildStreamDeserializerBody (single decode$Suffix dispatch). - Stream::readLines now guards str_replace(["\r\n", "\r"], "\n", ...) with str_contains($chunk, "\r"); NDJSON / text streams no longer pay the per-chunk rewrite when no CR bytes are present.
iamnamananand996
left a comment
There was a problem hiding this comment.
Looking at the seeds looks good
Closes FER-10530.
Summary
Adding PHP streaming support to match Python/Java/Go/etc.
Generated PHP SDKs now emit real streaming methods for endpoints with a
response-streaminstead of brokenvoidmethods that always throw.SSE,NDJSON, and raw text streaming all dispatch through a single unifiedStream<T>runtime helper (Go's strategy frominternal/streamer.go) with thinSseStream/JsonStream/TextStreamwrappers for clean named return types.What gets generated
For each
response-streamendpoint, the generator picks the format from the IR'sStreamingResponseunion (sse/json/text) and emits a method whose return type is one of the three wrappers below. All three share a buffer-cap and BOM-safe line reader; the differences are framing and constructor signature.SSE —
SseStream<T>Interface
Validates
Content-Type: text/event-streamand rejects non-UTF-8 charsets. Handles multi-linedata:(newline-joined), comment lines (:prefix), CRLF/CR normalization, and leading-space stripping per WHATWG §9.2.Generated method
Consumer usage
NDJSON —
JsonStream<T>Interface
One JSON document per line. Empty lines are skipped; a terminator line (if set) ends iteration cleanly.
Generated method
Consumer usage
Plain text —
TextStreamInterface
No deserializer — yields each line of the response body as a raw string. CRLF/CR normalized to LF; UTF-8 BOM stripped if present.
Generated method
Consumer usage (illustrative — no current fixture exercises text streaming end-to-end, but the codegen path is identical to NDJSON minus the deserializer)
Implementation
Stream.Template.phpemitted into every generated SDK with streaming endpoints, plus thinSseStream/JsonStream/TextStreamwrappers and anSseEvent<T>envelope for the metadata path.StreamingResponse._visit({ sse, json, text })is now consumed by both the return-type generator and the success-branch emitter; no IR change needed.ir.sdkConfig.hasStreamingEndpointsso SDKs without streaming endpoints are unaffected.StreamTest.Template.phpcovering the WHATWG SSE spec corners (multi-linedata:, comment lines, CRLF, terminator, charset validation) plus JSON/text framing.Test plan
pnpm seed test --generator php-sdk --fixture streaming— passes, NDJSON output now emitsJsonStream<StreamResponse>.pnpm seed test --generator php-sdk --fixture streaming-parameter— passes.pnpm seed test --generator php-sdk --fixture server-sent-events— passes, emitsSseStream<StreamedCompletion>.pnpm seed test --generator php-sdk --fixture server-sent-events-openapi— passes (removed fromallowedFailures).pnpm seed test --generator php-sdk --fixture server-sent-event-examples— passes.seed/php-sdk/server-sent-events(65 tests, 233 assertions).EventsClient::subscribe()now produces a real SSE method. Wrote an integration test exercising MockHttpClient → RawClient → EventsClient::subscribe() → SseStream → discriminated-union dispatch; all 4 cases pass.