From 1c60cfa55d4bd805f20a6ce0baf8331d8525e506 Mon Sep 17 00:00:00 2001 From: Ivan Pepelko Date: Thu, 14 Mar 2024 11:04:11 +0100 Subject: [PATCH] Implement Generator-based response streaming --- src/Http/StreamedResponse.php | 59 +++++++++++++++++++ src/RoadRunnerBridge/HttpFoundationWorker.php | 13 ++-- 2 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 src/Http/StreamedResponse.php diff --git a/src/Http/StreamedResponse.php b/src/Http/StreamedResponse.php new file mode 100644 index 0000000..e14756c --- /dev/null +++ b/src/Http/StreamedResponse.php @@ -0,0 +1,59 @@ +obControl = $obControl ?? function () { + if (\ob_get_status()) { + \ob_flush(); + } + \flush(); + }; + } + + public function getGenerator(): \Generator + { + if (!isset($this->callback)) { + throw new \LogicException('The Response callback must be set.'); + } + + $generator = ($this->callback)(); + if (!$generator instanceof \Generator) { + throw new \LogicException('The Response callback is not a valid generator.'); + } + + return $generator; + } + + public function sendContent(): static + { + if ($this->streamed) { + return $this; + } + + $this->streamed = true; + + if (!isset($this->callback)) { + throw new \LogicException('The Response callback must be set.'); + } + + foreach ($this->getGenerator() as $chunk) { + echo $chunk; + ($this->obControl)(); + } + + return $this; + } +} diff --git a/src/RoadRunnerBridge/HttpFoundationWorker.php b/src/RoadRunnerBridge/HttpFoundationWorker.php index 148493e..229997a 100644 --- a/src/RoadRunnerBridge/HttpFoundationWorker.php +++ b/src/RoadRunnerBridge/HttpFoundationWorker.php @@ -4,6 +4,7 @@ namespace Baldinof\RoadRunnerBundle\RoadRunnerBridge; +use Baldinof\RoadRunnerBundle\Http\StreamedResponse; use Spiral\RoadRunner\Http\HttpWorkerInterface; use Spiral\RoadRunner\Http\Request as RoadRunnerRequest; use Spiral\RoadRunner\WorkerInterface; @@ -11,7 +12,7 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; -use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpFoundation\StreamedResponse as SymfonyStreamedResponse; final class HttpFoundationWorker implements HttpFoundationWorkerInterface { @@ -42,8 +43,10 @@ public function respond(SymfonyResponse $symfonyResponse): void if ($content === false) { throw new \RuntimeException(sprintf("Cannot read file '%s'", $symfonyResponse->getFile()->getPathname())); // TODO: custom error } - } else { - if ($symfonyResponse instanceof StreamedResponse || $symfonyResponse instanceof BinaryFileResponse) { + } elseif ($symfonyResponse instanceof SymfonyStreamedResponse || $symfonyResponse instanceof BinaryFileResponse) { + if ($symfonyResponse instanceof StreamedResponse) { + $content = $symfonyResponse->getGenerator(); + } else { $content = ''; ob_start(function ($buffer) use (&$content) { $content .= $buffer; @@ -53,9 +56,9 @@ public function respond(SymfonyResponse $symfonyResponse): void $symfonyResponse->sendContent(); ob_end_clean(); - } else { - $content = (string) $symfonyResponse->getContent(); } + } else { + $content = (string) $symfonyResponse->getContent(); } $headers = $this->stringifyHeaders($symfonyResponse->headers->all());