Skip to content

Commit 9d7167d

Browse files
committed
docs: Add JsonStreamResponse documentation for 5.4
1 parent 5a61c1f commit 9d7167d

2 files changed

Lines changed: 143 additions & 0 deletions

File tree

docs/en/appendices/5-4-migration-guide.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ explicitly set `'strategy' => 'select'` when defining associations.
4242
`FormProtectionComponent`.
4343
See [Form Protection Component](../controllers/components/form-protection).
4444

45+
### Http
46+
47+
- Added `JsonStreamResponse` class for memory-efficient streaming of large JSON
48+
datasets using generators. Supports standard JSON arrays and NDJSON formats,
49+
envelope structures with metadata, transform callbacks, and graceful mid-stream
50+
error handling. See [Streaming JSON Responses](../controllers/request-response#streaming-json-responses).
51+
4552
### Database
4653

4754
- Added `notBetween()` method for `NOT BETWEEN` expressions.

docs/en/controllers/request-response.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,142 @@ public function sendIcs()
812812
}
813813
```
814814

815+
<a id="streaming-json-responses"></a>
816+
817+
### Streaming JSON Responses
818+
819+
`class` Cake\\Http\\Response\\**JsonStreamResponse**
820+
821+
When working with large datasets, loading everything into memory before encoding
822+
to JSON can exhaust available memory. `JsonStreamResponse` provides memory-efficient
823+
streaming of JSON data using generators, keeping only one item in memory at a time.
824+
825+
::: info Added in version 5.4.0
826+
:::
827+
828+
#### Basic Usage
829+
830+
```php
831+
use Cake\Http\Response\JsonStreamResponse;
832+
833+
public function index()
834+
{
835+
$query = $this->Articles->find();
836+
837+
// Simple array streaming
838+
return new JsonStreamResponse($query);
839+
// Output: [{"id":1,"title":"First"},{"id":2,"title":"Second"},...]
840+
}
841+
```
842+
843+
#### Constructor Options
844+
845+
The `JsonStreamResponse` constructor accepts an iterable and an options array:
846+
847+
| Option | Type | Default | Description |
848+
|--------|------|---------|-------------|
849+
| `root` | `string\|null` | `null` | Wrap data in `{"root": [...]}` |
850+
| `envelope` | `array` | `[]` | Static metadata merged with streaming data |
851+
| `dataKey` | `string` | `'data'` | Key for streaming data when envelope is used |
852+
| `format` | `string` | `'json'` | Output format: `'json'` or `'ndjson'` |
853+
| `transform` | `callable\|null` | `null` | Transform each item before encoding |
854+
| `flags` | `int` | `DEFAULT_JSON_FLAGS` | JSON encode flags |
855+
856+
#### With Root Wrapper
857+
858+
Wrap the array in an object with a named key:
859+
860+
```php
861+
return new JsonStreamResponse($query, ['root' => 'articles']);
862+
// Output: {"articles":[{"id":1,"title":"First"},{"id":2,"title":"Second"}]}
863+
```
864+
865+
#### With Envelope (Metadata)
866+
867+
Include static metadata alongside the streaming data:
868+
869+
```php
870+
$total = $this->Articles->find()->count();
871+
872+
return new JsonStreamResponse($query, [
873+
'envelope' => ['meta' => ['total' => $total, 'page' => 1]],
874+
'dataKey' => 'articles',
875+
]);
876+
// Output: {"meta":{"total":100,"page":1},"articles":[{"id":1,"title":"First"},...]}
877+
```
878+
879+
#### NDJSON Format
880+
881+
[NDJSON](http://ndjson.org/) (Newline Delimited JSON) outputs one JSON object per
882+
line, useful for streaming to clients that process data incrementally:
883+
884+
```php
885+
return new JsonStreamResponse($query, ['format' => 'ndjson']);
886+
// Output:
887+
// {"id":1,"title":"First"}
888+
// {"id":2,"title":"Second"}
889+
```
890+
891+
The content type is automatically set to `application/x-ndjson; charset=UTF-8`.
892+
893+
#### Transform Callback
894+
895+
Transform each item before JSON encoding. Useful for selecting specific fields
896+
or formatting data:
897+
898+
```php
899+
return new JsonStreamResponse($query, [
900+
'transform' => fn($article) => [
901+
'id' => $article->id,
902+
'title' => $article->title,
903+
'url' => Router::url(['action' => 'view', $article->id]),
904+
],
905+
]);
906+
```
907+
908+
#### Immutability
909+
910+
`JsonStreamResponse` follows PSR-7 immutability patterns. Use `withStreamOptions()`
911+
to create a modified copy:
912+
913+
```php
914+
$response = new JsonStreamResponse($query);
915+
$newResponse = $response->withStreamOptions(['root' => 'articles']);
916+
```
917+
918+
#### Error Handling
919+
920+
`JsonStreamResponse` uses a three-layer error handling strategy:
921+
922+
1. **Pre-validation**: The first item is encoded before output starts. If encoding
923+
fails, an exception is thrown and a proper error response can be returned.
924+
925+
2. **Mid-stream error marker**: If item N (where N > 1) fails to encode, an error
926+
marker is output to maintain valid JSON structure:
927+
```json
928+
[{"id":1},{"__streamError":{"message":"Type is not supported","index":1}}]
929+
```
930+
931+
3. **Server-side logging**: All encoding failures are logged via `Log::error()`.
932+
933+
#### ORM Integration
934+
935+
For true memory-efficient streaming, use unbuffered queries and avoid result
936+
formatters:
937+
938+
```php
939+
// Good - streams one row at a time
940+
$query = $this->Articles->find()->bufferResults(false);
941+
return new JsonStreamResponse($query);
942+
943+
// Avoid - formatters like map(), combine() buffer results internally
944+
$query = $this->Articles->find()->map(fn($row) => $row); // Breaks streaming
945+
```
946+
947+
> [!NOTE]
948+
> Result formatters (`map()`, `combine()`, etc.) buffer results internally,
949+
> which defeats the memory-efficient streaming purpose.
950+
815951
### Setting Headers
816952

817953
`method` Cake\\Http\\Response::**withHeader**(string $name, string|array $value): static

0 commit comments

Comments
 (0)