1717use Closure ;
1818use Deprecated ;
1919use Exception ;
20+ use Generator ;
2021use InvalidArgumentException ;
2122use Iterator ;
2223use JsonException ;
2627use TypeError ;
2728
2829use function array_filter ;
30+ use function array_map ;
2931use function array_reduce ;
32+ use function array_values ;
3033use function get_defined_constants ;
34+ use function implode ;
35+ use function in_array ;
3136use function is_bool ;
3237use function is_resource ;
3338use function is_string ;
@@ -154,7 +159,7 @@ public function __construct(
154159 $ end = '] ' ;
155160 $ separator = ', ' ;
156161 $ chunkFormatter = fn (array $ value ): array => $ value ;
157- if (JsonFormat::NdJson ! == $ this ->format ) {
162+ if (JsonFormat::Standard = == $ this ->format ) {
158163 $ chunkFormatter = array_values (...);
159164 }
160165
@@ -165,18 +170,18 @@ public function __construct(
165170 $ chunkFormatter = fn (array $ value ): array => $ value ;
166171 }
167172
168- if (JsonFormat::NdJson = == $ this ->format ) {
173+ if (JsonFormat::Standard ! == $ this ->format ) {
169174 $ start = '' ;
170175 $ end = "\n" ;
171176 $ separator = "\n" ;
172177 }
173178
174- $ this ->emptyIterable = JsonFormat::NdJson = == $ this ->format ? '' : $ start .$ end ;
179+ $ this ->emptyIterable = JsonFormat::Standard ! == $ this ->format ? '' : $ start .$ end ;
175180 if ($ this ->usePrettyPrint ()) {
176181 $ start .= "\n" ;
177182 $ end = "\n" .$ end ;
178183 $ separator .= "\n" ;
179- if (JsonFormat::NdJson = == $ this ->format ) {
184+ if (JsonFormat::Standard ! == $ this ->format ) {
180185 $ start = '' ;
181186 $ end = "\n" ;
182187 $ separator = "\n" ;
@@ -189,9 +194,10 @@ public function __construct(
189194 $ this ->start = $ start ;
190195 $ this ->end = $ end ;
191196 $ this ->separator = $ separator ;
192- $ this ->jsonEncodeChunk = (JsonFormat::NdJson === $ this ->format )
193- ? fn (array $ chunk ): string => implode ($ this ->separator , array_map (fn ($ value ) => json_encode (($ chunkFormatter )($ value ), $ flags , $ this ->depth ), $ chunk ))
194- : fn (array $ chunk ): string => ($ prettyPrintFormatter )(substr (json_encode (($ chunkFormatter )($ chunk ), $ flags , $ this ->depth ), /* @phpstan-ignore-line */ 1 , -1 ));
197+ $ this ->jsonEncodeChunk = match ($ this ->format ) {
198+ JsonFormat::Standard => fn (array $ chunk ): string => ($ prettyPrintFormatter )(substr (json_encode (($ chunkFormatter )($ chunk ), $ flags , $ this ->depth ), /* @phpstan-ignore-line */ 1 , -1 )),
199+ default => fn (array $ chunk ): string => implode ($ this ->separator , array_map (fn ($ value ) => json_encode (($ chunkFormatter )($ value ), $ flags , $ this ->depth ), $ chunk )),
200+ };
195201 }
196202
197203 /**
@@ -391,32 +397,38 @@ public function when(callable|bool $condition, callable $onSuccess, ?callable $o
391397 * Returns the number of characters read from the handle and passed through to the output.
392398 *
393399 * @param iterable<T> $records
400+ * @param array<string> $header
394401 *
395402 * @throws Exception
396403 * @throws JsonException
397404 */
398- public function download (iterable $ records , ?string $ filename = null ): int
405+ public function download (iterable | TabularDataProvider $ records , ?string $ filename = null , array $ header = [] ): int
399406 {
400407 if (null !== $ filename ) {
401- $ mimetype = JsonFormat::NdJson === $ this ->format ? 'application/jsonl ' : 'application/json ' ;
408+ $ mimetype = JsonFormat::Standard === $ this ->format ? 'application/json ' : 'application/x-ndjson ' ;
402409 HttpHeaders::forFileDownload ($ filename , $ mimetype .'; charset=utf-8 ' );
403410 }
404411
405- return $ this ->save ($ records , new SplFileObject ('php://output ' , 'wb ' ));
412+ return $ this ->save (
413+ records: $ records ,
414+ destination: new SplFileObject ('php://output ' , 'wb ' ),
415+ header: $ header ,
416+ );
406417 }
407418
408419 /**
409420 * Returns the JSON representation of a tabular data collection.
410421 *
411422 * @param iterable<T> $records
423+ * @param array<string> $header
412424 *
413425 * @throws Exception
414426 * @throws JsonException
415427 */
416- public function encode (iterable $ records ): string
428+ public function encode (iterable $ records, array $ header = [] ): string
417429 {
418430 $ stream = Stream::createFromString ();
419- $ this ->save ($ records , $ stream );
431+ $ this ->save (records: $ records , destination: $ stream, header: $ header );
420432 $ stream ->rewind ();
421433
422434 return (string ) $ stream ->getContents ();
@@ -434,13 +446,14 @@ public function encode(iterable $records): string
434446 * @param iterable<T> $records
435447 * @param SplFileInfo|SplFileObject|Stream|resource|string $destination
436448 * @param resource|null $context
449+ * @param array<string> $header
437450 *
438451 * @throws JsonException
439452 * @throws RuntimeException
440453 * @throws TypeError
441454 * @throws UnavailableStream
442455 */
443- public function save (iterable $ records , mixed $ destination , $ context = null ): int
456+ public function save (iterable | TabularDataProvider $ records , mixed $ destination , $ context = null , array $ header = [] ): int
444457 {
445458 $ stream = match (true ) {
446459 $ destination instanceof Stream,
@@ -452,7 +465,7 @@ public function save(iterable $records, mixed $destination, $context = null): in
452465 };
453466 $ bytes = 0 ;
454467 $ writtenBytes = 0 ;
455- foreach ($ this ->convert ($ records ) as $ line ) {
468+ foreach ($ this ->convert ($ records, $ header ) as $ line ) {
456469 /** @var int|false $writtenBytes */
457470 $ writtenBytes = Warning::cloak ($ stream ->fwrite (...), $ line );
458471 if (false === $ writtenBytes ) {
@@ -470,19 +483,36 @@ public function save(iterable $records, mixed $destination, $context = null): in
470483 * Returns an Iterator that you can iterate to generate the actual JSON string representation.
471484 *
472485 * @param iterable<T> $records
486+ * @param array<string> $header
473487 *
474488 * @throws JsonException
475489 * @throws Exception
476490 *
477491 * @return Iterator<string>
478492 */
479- public function convert (iterable $ records ): Iterator
493+ public function convert (iterable | TabularDataProvider $ records, array $ header = [] ): Iterator
480494 {
495+ if ($ records instanceof TabularDataProvider) {
496+ $ tabularData = $ records ->getTabularData ();
497+ $ records = $ tabularData ->getRecords ();
498+ if ([] === $ header ) {
499+ $ header = $ tabularData ->getHeader ();
500+ }
501+ }
502+
481503 $ iterator = match ($ this ->formatter ) {
482504 null => MapIterator::toIterator ($ records ),
483505 default => MapIterator::fromIterable ($ records , $ this ->formatter )
484506 };
485507
508+ if (in_array ($ this ->format , [JsonFormat::NdJsonHeader, JsonFormat::NdJsonHeaderLess], true )) {
509+ if ($ records instanceof TabularData && [] === $ header ) {
510+ $ header = $ records ->getHeader ();
511+ }
512+
513+ $ iterator = self ::getList ($ iterator , $ header , $ this ->format )();
514+ }
515+
486516 $ iterator ->rewind ();
487517 if (!$ iterator ->valid ()) {
488518 yield $ this ->emptyIterable ;
@@ -496,7 +526,7 @@ public function convert(iterable $records): Iterator
496526 $ current = $ iterator ->current ();
497527 $ iterator ->next ();
498528
499- if (JsonFormat::NdJson ! == $ this ->format ) {
529+ if (JsonFormat::Standard = == $ this ->format ) {
500530 yield $ this ->start ;
501531 }
502532
@@ -522,6 +552,28 @@ public function convert(iterable $records): Iterator
522552 yield ($ this ->jsonEncodeChunk )([$ offset => $ current ]).$ this ->end ;
523553 }
524554
555+ /**
556+ * @param array<string> $header
557+ *
558+ * @throws InvalidArgument
559+ *
560+ * @return Closure(): Generator
561+ */
562+ private static function getList (Iterator $ data , array $ header , JsonFormat $ format ): Closure
563+ {
564+ if (JsonFormat::NdJsonHeaderLess === $ format ) {
565+ return fn () => yield from new MapIterator ($ data , fn (array $ record ): array => array_values ($ record ));
566+ }
567+
568+ [] !== $ header || throw new InvalidArgument ('the tabular data header is empty. ' );
569+
570+ return function () use ($ header , $ data ) {
571+ yield $ header ;
572+
573+ yield from new MapIterator ($ data , fn (array $ record ): array => array_values ($ record ));
574+ };
575+ }
576+
525577 /**
526578 * DEPRECATION WARNING! This method will be removed in the next major point release.
527579 *
0 commit comments