Skip to content

Commit 280e92d

Browse files
committed
support root-level Generator in StreamedJsonResponse
1 parent 73a6b4b commit 280e92d

File tree

2 files changed

+78
-42
lines changed

2 files changed

+78
-42
lines changed

src/Symfony/Component/HttpFoundation/StreamedJsonResponse.php

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ class StreamedJsonResponse extends StreamedResponse
4747
private const PLACEHOLDER = '__symfony_json__';
4848

4949
/**
50-
* @param mixed[] $data JSON Data containing PHP generators which will be streamed as list of data
50+
* @param mixed[] $data JSON Data containing PHP generators which will be streamed as list of data or a Generator
5151
* @param int $status The HTTP status code (200 "OK" by default)
5252
* @param array<string, string|string[]> $headers An array of HTTP headers
5353
* @param int $encodingOptions Flags for the json_encode() function
5454
*/
5555
public function __construct(
56-
private readonly array $data,
56+
private readonly iterable $data,
5757
int $status = 200,
5858
array $headers = [],
5959
private int $encodingOptions = JsonResponse::DEFAULT_ENCODING_OPTIONS,
@@ -66,11 +66,35 @@ public function __construct(
6666
}
6767

6868
private function stream(): void
69+
{
70+
$jsonEncodingOptions = \JSON_THROW_ON_ERROR | $this->encodingOptions;
71+
$keyEncodingOptions = $jsonEncodingOptions & ~\JSON_NUMERIC_CHECK;
72+
73+
$this->streamData($this->data, $jsonEncodingOptions, $keyEncodingOptions);
74+
}
75+
76+
private function streamData(mixed $data, int $jsonEncodingOptions, int $keyEncodingOptions): void
77+
{
78+
if (is_array($data)) {
79+
$this->streamArray($data, $jsonEncodingOptions, $keyEncodingOptions);
80+
81+
return;
82+
}
83+
84+
if (is_iterable($data) && ! $data instanceof \JsonSerializable) {
85+
$this->streamIterable($data, $jsonEncodingOptions, $keyEncodingOptions);
86+
87+
return;
88+
}
89+
90+
echo json_encode($data, $jsonEncodingOptions);
91+
}
92+
93+
private function streamArray(array $data, int $jsonEncodingOptions, int $keyEncodingOptions): void
6994
{
7095
$generators = [];
71-
$structure = $this->data;
7296

73-
array_walk_recursive($structure, function (&$item, $key) use (&$generators) {
97+
array_walk_recursive($data, function (&$item, $key) use (&$generators) {
7498
if (self::PLACEHOLDER === $key) {
7599
// if the placeholder is already in the structure it should be replaced with a new one that explode
76100
// works like expected for the structure
@@ -88,56 +112,51 @@ private function stream(): void
88112
}
89113
});
90114

91-
$jsonEncodingOptions = \JSON_THROW_ON_ERROR | $this->encodingOptions;
92-
$keyEncodingOptions = $jsonEncodingOptions & ~\JSON_NUMERIC_CHECK;
93-
94-
$jsonParts = explode('"'.self::PLACEHOLDER.'"', json_encode($structure, $jsonEncodingOptions));
115+
$jsonParts = explode('"'.self::PLACEHOLDER.'"', json_encode($data, $jsonEncodingOptions));
95116

96117
foreach ($generators as $index => $generator) {
97118
// send first and between parts of the structure
98119
echo $jsonParts[$index];
99120

100-
if ($generator instanceof \JsonSerializable || !$generator instanceof \Traversable) {
101-
// the placeholders, JsonSerializable and none traversable items in the structure are rendered here
102-
echo json_encode($generator, $jsonEncodingOptions);
103-
104-
continue;
105-
}
121+
$this->streamData($generator, $jsonEncodingOptions, $keyEncodingOptions);
122+
}
106123

107-
$isFirstItem = true;
108-
$startTag = '[';
109-
110-
foreach ($generator as $key => $item) {
111-
if ($isFirstItem) {
112-
$isFirstItem = false;
113-
// depending on the first elements key the generator is detected as a list or map
114-
// we can not check for a whole list or map because that would hurt the performance
115-
// of the streamed response which is the main goal of this response class
116-
if (0 !== $key) {
117-
$startTag = '{';
118-
}
119-
120-
echo $startTag;
121-
} else {
122-
// if not first element of the generic, a separator is required between the elements
123-
echo ',';
124-
}
124+
// send last part of the structure
125+
echo $jsonParts[array_key_last($jsonParts)];
126+
}
125127

126-
if ('{' === $startTag) {
127-
echo json_encode((string) $key, $keyEncodingOptions).':';
128+
private function streamIterable(iterable $iterable, int $jsonEncodingOptions, int $keyEncodingOptions): void
129+
{
130+
$isFirstItem = true;
131+
$startTag = '[';
132+
133+
foreach ($iterable as $key => $item) {
134+
if ($isFirstItem) {
135+
$isFirstItem = false;
136+
// depending on the first elements key the generator is detected as a list or map
137+
// we can not check for a whole list or map because that would hurt the performance
138+
// of the streamed response which is the main goal of this response class
139+
if (0 !== $key) {
140+
$startTag = '{';
128141
}
129142

130-
echo json_encode($item, $jsonEncodingOptions);
143+
echo $startTag;
144+
} else {
145+
// if not first element of the generic, a separator is required between the elements
146+
echo ',';
131147
}
132148

133-
if ($isFirstItem) { // indicates that the generator was empty
134-
echo '[';
149+
if ('{' === $startTag) {
150+
echo json_encode((string) $key, $keyEncodingOptions).':';
135151
}
136152

137-
echo '[' === $startTag ? ']' : '}';
153+
$this->streamData($item, $jsonEncodingOptions, $keyEncodingOptions);
138154
}
139155

140-
// send last part of the structure
141-
echo $jsonParts[array_key_last($jsonParts)];
156+
if ($isFirstItem) { // indicates that the generator was empty
157+
echo '[';
158+
}
159+
160+
echo '[' === $startTag ? ']' : '}';
142161
}
143162
}

src/Symfony/Component/HttpFoundation/Tests/StreamedJsonResponseTest.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,23 @@ public function testResponseSimpleList()
3030
$this->assertSame('{"_embedded":{"articles":["Article 1","Article 2","Article 3"],"news":["News 1","News 2","News 3"]}}', $content);
3131
}
3232

33+
public function testResponseSimpleGenerator()
34+
{
35+
$content = $this->createSendResponse($this->generatorSimple('Article'));
36+
37+
$this->assertSame('["Article 1","Article 2","Article 3"]', $content);
38+
}
39+
40+
public function testResponseNestedGenerator()
41+
{
42+
$content = $this->createSendResponse((function (): iterable {
43+
yield 'articles' => $this->generatorSimple('Article');
44+
yield 'news' => $this->generatorSimple('News');
45+
})());
46+
47+
$this->assertSame('{"articles":["Article 1","Article 2","Article 3"],"news":["News 1","News 2","News 3"]}', $content);
48+
}
49+
3350
public function testResponseEmptyList()
3451
{
3552
$content = $this->createSendResponse(
@@ -220,9 +237,9 @@ public function testEncodingOptions()
220237
}
221238

222239
/**
223-
* @param mixed[] $data
240+
* @param iterable<mixed> $data
224241
*/
225-
private function createSendResponse(array $data): string
242+
private function createSendResponse(iterable $data): string
226243
{
227244
$response = new StreamedJsonResponse($data);
228245

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy