@@ -47,13 +47,13 @@ class StreamedJsonResponse extends StreamedResponse
4747private 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 */
5555public function __construct (
56- private readonly array $ data ,
56+ private readonly iterable $ data ,
5757int $ status =200 ,
5858array $ headers = [],
5959private int $ encodingOptions = JsonResponse::DEFAULT_ENCODING_OPTIONS ,
@@ -66,11 +66,35 @@ public function __construct(
6666 }
6767
6868private 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 ) && !$ datainstanceof \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 ) {
7498if (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
96117foreach ($ generatorsas $ index =>$ generator ) {
97118// send first and between parts of the structure
98119echo $ jsonParts [$ index ];
99120
100- if ($ generatorinstanceof \JsonSerializable || !$ generatorinstanceof \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 ($ generatoras $ 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 ($ iterableas $ 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}