@@ -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}
0 commit comments