@@ -631,6 +631,103 @@ represented by a PHP callable instead of a string::
631
631
632
632
.. _component-http-foundation-serving-files :
633
633
634
+ Streaming a JSON Response
635
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
636
+
637
+ .. versionadded :: 6.3
638
+
639
+ The :class: `Symfony\\ Component\\ HttpFoundation\\ StreamedJsonResponse ` class was
640
+ introduced in Symfony 6.3.
641
+
642
+ The :class: `Symfony\\ Component\\ HttpFoundation\\ StreamedJsonResponse ` allows an API to
643
+ return a lot of data as JSON and keep the used resources low by make usage of Generators.
644
+
645
+ It expects an array which represents the JSON structure and the list which should be
646
+ streamed are represented in the array as ``\Generator ``. It also supports any kind of
647
+ Traversable containing JSON serializable data for a good developer experience,
648
+ but for keep the resources usage as low as possible, it is recommended to use ``\Generators ``,
649
+ as they the advantages that only the current returned data need to be keep in memory.
650
+
651
+ The response will stream the JSON with generators in to most efficient way and keep resources as low as possible::
652
+
653
+ use Symfony\Component\HttpFoundation\StreamedJsonResponse;
654
+
655
+ function loadArticles(): \Generator { // any method or function returning a Generator
656
+ yield ['title' => 'Article 1'];
657
+ yield ['title' => 'Article 2'];
658
+ yield ['title' => 'Article 3'];
659
+ };
660
+
661
+ $response = new StreamedJsonResponse(
662
+ // json structure with generators in which will be streamed as a list
663
+ [
664
+ '_embedded' => [
665
+ 'articles' => loadArticles(), // any \Generator can be used which will be streamed as list of data
666
+ ],
667
+ ],
668
+ );
669
+
670
+ .. tip ::
671
+
672
+ If loading data via doctrine the ``toIterable `` method of ``Doctrine `` can be
673
+ used to keep the resources low and fetch only row by row.
674
+ See the `Doctrine Batch processing `_ documentation for more::
675
+
676
+ public function __invoke(): Response
677
+ {
678
+ return new StreamedJsonResponse(
679
+ [
680
+ '_embedded' => [
681
+ 'articles' => $this->loadArticles(),
682
+ ],
683
+ ],
684
+ );
685
+ }
686
+
687
+ public function loadArticles(): \Generator
688
+ {
689
+ $queryBuilder = $entityManager->createQueryBuilder();
690
+ $queryBuilder->from(Article::class, 'article');
691
+ $queryBuilder->select('article.id')
692
+ ->addSelect('article.title')
693
+ ->addSelect('article.description');
694
+
695
+ return $queryBuilder->getQuery()->toIterable();
696
+ }
697
+
698
+ .. tip ::
699
+
700
+ If you have a lot of data to be returned you may want to call the
701
+ PHP `flush <https://www.php.net/manual/en/function.flush.php >`__ method between
702
+ to flush the response after every specific count of items::
703
+
704
+ public function __invoke(): Response
705
+ {
706
+ return new StreamedJsonResponse([
707
+ '_embedded' => [
708
+ 'articles' => $this->loadArticles(),
709
+ ],
710
+ ]);
711
+ }
712
+
713
+ public function loadArticles(): \Generator
714
+ {
715
+ $queryBuilder = $entityManager->createQueryBuilder();
716
+ $queryBuilder->from(Article::class, 'article');
717
+ $queryBuilder->select('article.id')
718
+ ->addSelect('article.title')
719
+ ->addSelect('article.description');
720
+
721
+ $count = 0;
722
+ foreach ($queryBuilder->getQuery()->toIterable() as $article) {
723
+ yield $article;
724
+
725
+ if (++$count % 100 === 0) {
726
+ flush();
727
+ }
728
+ }
729
+ }
730
+
634
731
Serving Files
635
732
~~~~~~~~~~~~~
636
733
@@ -866,3 +963,4 @@ Learn More
866
963
.. _`JSON Hijacking` : https://haacked.com/archive/2009/06/25/json-hijacking.aspx/
867
964
.. _OWASP guidelines : https://cheatsheetseries.owasp.org/cheatsheets/AJAX_Security_Cheat_Sheet.html#always-return-json-with-an-object-on-the-outside
868
965
.. _RFC 8674 : https://tools.ietf.org/html/rfc8674
966
+ .. _Doctrine Batch processing : https://www.doctrine-project.org/projects/doctrine-orm/en/2.14/reference/batch-processing.html#iterating-results
0 commit comments