@@ -631,6 +631,103 @@ represented by a PHP callable instead of a string::
631631
632632.. _component-http-foundation-serving-files :
633633
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+
634731Serving Files
635732~~~~~~~~~~~~~
636733
@@ -866,3 +963,4 @@ Learn More
866963.. _`JSON Hijacking` : https://haacked.com/archive/2009/06/25/json-hijacking.aspx/
867964.. _OWASP guidelines : https://cheatsheetseries.owasp.org/cheatsheets/AJAX_Security_Cheat_Sheet.html#always-return-json-with-an-object-on-the-outside
868965.. _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