@@ -47,13 +47,13 @@ class StreamedJsonResponse extends StreamedResponse
47
47
private const PLACEHOLDER = '__symfony_json__ ' ;
48
48
49
49
/**
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
51
51
* @param int $status The HTTP status code (200 "OK" by default)
52
52
* @param array<string, string|string[]> $headers An array of HTTP headers
53
53
* @param int $encodingOptions Flags for the json_encode() function
54
54
*/
55
55
public function __construct (
56
- private readonly array $ data ,
56
+ private readonly iterable $ data ,
57
57
int $ status = 200 ,
58
58
array $ headers = [],
59
59
private int $ encodingOptions = JsonResponse::DEFAULT_ENCODING_OPTIONS ,
@@ -66,11 +66,35 @@ public function __construct(
66
66
}
67
67
68
68
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
69
94
{
70
95
$ generators = [];
71
- $ structure = $ this ->data ;
72
96
73
- array_walk_recursive ($ structure , function (&$ item , $ key ) use (&$ generators ) {
97
+ array_walk_recursive ($ data , function (&$ item , $ key ) use (&$ generators ) {
74
98
if (self ::PLACEHOLDER === $ key ) {
75
99
// if the placeholder is already in the structure it should be replaced with a new one that explode
76
100
// works like expected for the structure
@@ -88,56 +112,51 @@ private function stream(): void
88
112
}
89
113
});
90
114
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 ));
95
116
96
117
foreach ($ generators as $ index => $ generator ) {
97
118
// send first and between parts of the structure
98
119
echo $ jsonParts [$ index ];
99
120
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
+ }
106
123
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
+ }
125
127
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 = '{ ' ;
128
141
}
129
142
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 ', ' ;
131
147
}
132
148
133
- if ($ isFirstItem ) { // indicates that the generator was empty
134
- echo ' [ ' ;
149
+ if (' { ' === $ startTag ) {
150
+ echo json_encode (( string ) $ key , $ keyEncodingOptions ). ' : ' ;
135
151
}
136
152
137
- echo ' [ ' === $ startTag ? ' ] ' : ' } ' ;
153
+ $ this -> streamData ( $ item , $ jsonEncodingOptions , $ keyEncodingOptions ) ;
138
154
}
139
155
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 ? '] ' : '} ' ;
142
161
}
143
162
}
0 commit comments