77use Cake \Datasource \EntityInterface ;
88use Cake \Utility \Hash ;
99use Cake \View \SerializedView ;
10+ use Stringable ;
1011
1112/**
1213 * A view class that is used for CSV responses.
@@ -77,11 +78,18 @@ class CsvView extends SerializedView
7778 protected string $ subDir = 'csv ' ;
7879
7980 /**
80- * Whether or not to reset static variables in use
81+ * Aggregated CSV output for the current serialization pass.
8182 *
82- * @var bool
83+ * @var string
84+ */
85+ protected string $ csv = '' ;
86+
87+ /**
88+ * Temp stream used by fputcsv() to generate a single row.
89+ *
90+ * @var resource|null
8391 */
84- protected bool $ _resetStaticVariables = false ;
92+ protected $ fp = null ;
8593
8694 /**
8795 * Iconv extension.
@@ -201,16 +209,43 @@ public static function contentType(): string
201209 */
202210 protected function _serialize (array |string $ serialize ): string
203211 {
212+ $ this ->resetState ();
213+
204214 $ this ->_renderRow ($ this ->getConfig ('header ' ));
205215 $ this ->_renderContent ();
206216 $ this ->_renderRow ($ this ->getConfig ('footer ' ));
207- $ content = $ this ->_renderRow () ;
208- $ this -> _resetStaticVariables = true ;
209- $ this ->_renderRow ();
217+ $ content = $ this ->csv ;
218+
219+ $ this ->resetState ();
210220
211221 return $ content ;
212222 }
213223
224+ /**
225+ * Reset accumulated state so the same view instance can render multiple
226+ * times in a single request (queue worker, multi-file export, etc.).
227+ */
228+ protected function resetState (): void
229+ {
230+ $ this ->csv = '' ;
231+ $ this ->isFirstBom = true ;
232+ if (is_resource ($ this ->fp )) {
233+ fclose ($ this ->fp );
234+ }
235+ $ this ->fp = null ;
236+ }
237+
238+ /**
239+ * @inheritDoc
240+ */
241+ public function __destruct ()
242+ {
243+ if (is_resource ($ this ->fp )) {
244+ fclose ($ this ->fp );
245+ $ this ->fp = null ;
246+ }
247+ }
248+
214249 /**
215250 * Renders the body of the data to the csv
216251 *
@@ -245,24 +280,35 @@ protected function _renderContent(): void
245280 foreach ($ extract as $ formatter ) {
246281 if (!is_string ($ formatter ) && is_callable ($ formatter )) {
247282 $ value = $ formatter ($ _data );
283+ $ pathForError = '<callable> ' ;
248284 } else {
249285 $ path = $ formatter ;
250286 $ format = null ;
251287 if (is_array ($ formatter )) {
252288 [$ path , $ format ] = $ formatter ;
253289 }
290+ $ pathForError = (string )$ path ;
254291
255- if (!str_contains ($ path , '. ' )) {
256- $ value = $ _data [$ path ];
257- } else {
258- $ value = Hash::get ($ _data , $ path );
259- }
292+ $ value = Hash::get ($ _data , $ path );
260293
261- if ($ format ) {
294+ if ($ format !== null ) {
262295 $ value = sprintf ($ format , $ value );
263296 }
264297 }
265298
299+ if (
300+ $ value !== null
301+ && !is_scalar ($ value )
302+ && !($ value instanceof Stringable)
303+ ) {
304+ throw new CakeException (sprintf (
305+ 'Extract path `%s` resolved to a non-scalar `%s`. '
306+ . 'Use a callable formatter to flatten it, or adjust the extract path. ' ,
307+ $ pathForError ,
308+ get_debug_type ($ value ),
309+ ));
310+ }
311+
266312 $ values [] = $ value ;
267313 }
268314 $ this ->_renderRow ($ values );
@@ -273,54 +319,44 @@ protected function _renderContent(): void
273319 /**
274320 * Aggregates the rows into a single csv
275321 *
276- * @param array<string >|null $row Row data
322+ * @param array<scalar|\Stringable|null >|null $row Row data
277323 * @return string CSV with all data to date
278324 */
279325 protected function _renderRow (?array $ row = null ): string
280326 {
281- static $ csv = '' ;
327+ $ this -> csv .= ( string ) $ this -> _generateRow ( $ row ) ;
282328
283- if ($ this ->_resetStaticVariables ) {
284- $ csv = '' ;
285- $ this ->_resetStaticVariables = false ;
286-
287- return '' ;
288- }
289-
290- $ csv .= (string )$ this ->_generateRow ($ row );
291-
292- return $ csv ;
329+ return $ this ->csv ;
293330 }
294331
295332 /**
296333 * Generates a single row in a csv from an array of
297334 * data by writing the array to a temporary file and
298335 * returning its contents
299336 *
300- * @param array<string |null>|null $row Row data
337+ * @param array<scalar|\Stringable |null>|null $row Row data
301338 * @return string|false String with the row in csv-syntax, false on fputscv failure
302339 */
303340 protected function _generateRow (?array $ row = null ): string |false
304341 {
305- static $ fp = false ;
306-
307342 if (!$ row ) {
308343 return '' ;
309344 }
310345
311- if ($ fp === false ) {
346+ if ($ this -> fp === null ) {
312347 $ stream = 'php://temp ' ;
313348 $ fp = fopen ($ stream , 'r+ ' );
314349 if ($ fp === false ) {
315350 throw new CakeException (sprintf ('Cannot open stream `%s` ' , $ stream ));
316351 }
352+ $ this ->fp = $ fp ;
317353
318354 $ setSeparator = $ this ->getConfig ('setSeparator ' );
319355 if ($ setSeparator ) {
320- fwrite ($ fp , 'sep= ' . $ setSeparator . "\n" );
356+ fwrite ($ this -> fp , 'sep= ' . $ setSeparator . "\n" );
321357 }
322358 } else {
323- ftruncate ($ fp , 0 );
359+ ftruncate ($ this -> fp , 0 );
324360 }
325361
326362 $ null = $ this ->getConfig ('null ' );
@@ -340,21 +376,21 @@ protected function _generateRow(?array $row = null): string|false
340376 /** @phpstan-ignore-next-line */
341377 $ row = str_replace (["\r\n" , "\n" , "\r" ], $ newline , $ row );
342378 if ($ enclosure === '' ) {
343- // fputcsv does not supports empty enclosure
344- if (fputs ($ fp , implode ($ delimiter , $ row ) . "\n" ) === false ) {
379+ // fputcsv does not support empty enclosure
380+ if (fputs ($ this -> fp , implode ($ delimiter , $ row ) . "\n" ) === false ) {
345381 return false ;
346382 }
347383 } else {
348- if (fputcsv ($ fp , $ row , $ delimiter , $ enclosure , $ escape ) === false ) {
384+ if (fputcsv ($ this -> fp , $ row , $ delimiter , $ enclosure , $ escape ) === false ) {
349385 return false ;
350386 }
351387 }
352388
353- rewind ($ fp );
389+ rewind ($ this -> fp );
354390 unset($ row );
355391
356392 $ csv = '' ;
357- while (($ buffer = fgets ($ fp , 4096 )) !== false ) {
393+ while (($ buffer = fgets ($ this -> fp , 4096 )) !== false ) {
358394 $ csv .= $ buffer ;
359395 }
360396
0 commit comments