-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[HttpFoundation] added support for streamed responses #2935
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
0038d1b
e44b8ba
8717d44
473741b
887c0e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
To stream a Response, use the StreamedResponse class instead of the standard Response class: $response = new StreamedResponse(function () { echo 'FOO'; }); $response = new StreamedResponse(function () { echo 'FOO'; }, 200, array('Content-Type' => 'text/plain')); As you can see, a StreamedResponse instance takes a PHP callback instead of a string for the Response content. It's up to the developer to stream the response content from the callback with standard PHP functions like echo. You can also use flush() if needed. From a controller, do something like this: $twig = $this->get('templating'); return new StreamedResponse(function () use ($templating) { $templating->stream('BlogBundle:Annot:streamed.html.twig'); }, 200, array('Content-Type' => 'text/html')); If you are using the base controller, you can use the stream() method instead: return $this->stream('BlogBundle:Annot:streamed.html.twig'); You can stream an existing file by using the PHP built-in readfile() function: new StreamedResponse(function () use ($file) { readfile($file); }, 200, array('Content-Type' => 'image/png'); Read http://php.net/flush for more information about output buffering in PHP. Note that you should do your best to move all expensive operations to be "activated/evaluated/called" during template evaluation. Templates --------- If you are using Twig as a template engine, everything should work as usual, even if are using template inheritance! However, note that streaming is not supported for PHP templates. Support is impossible by design (as the layout is rendered after the main content). Exceptions ---------- Exceptions thrown during rendering will be rendered as usual except that some content might have been rendered already. Limitations ----------- As the getContent() method always returns false for streamed Responses, some event listeners won't work at all: * Web debug toolbar is not available for such Responses (but the profiler works fine); * ESI is not supported. Also note that streamed responses cannot benefit from HTTP caching for obvious reasons.
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ | |
|
||
use Symfony\Component\HttpFoundation\Response; | ||
use Symfony\Component\HttpFoundation\RedirectResponse; | ||
use Symfony\Component\HttpFoundation\StreamedResponse; | ||
use Symfony\Component\DependencyInjection\ContainerAware; | ||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; | ||
use Symfony\Component\Form\FormTypeInterface; | ||
|
@@ -98,6 +99,24 @@ public function render($view, array $parameters = array(), Response $response = | |
return $this->container->get('templating')->renderResponse($view, $parameters, $response); | ||
} | ||
|
||
/** | ||
* Streams a view. | ||
* | ||
* @param string $view The view name | ||
* @param array $parameters An array of parameters to pass to the view | ||
* @param Response $response A response instance | ||
* | ||
* @return StreamedResponse A StreamedResponse instance | ||
*/ | ||
public function stream($view, array $parameters = array(), Response $response = null) | ||
{ | ||
$templating = $this->container->get('templating'); | ||
|
||
return new StreamedResponse(function () use ($templating, $view, $parameters) { | ||
$templating->stream($view, $parameters); | ||
}, null === $response ? 200 : $response->getStatusCode(), null === $response ? array() : $response->headers->all()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there a specific reason why you don't want to assign these parameters to variables for better readability? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no specific reason. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. then i think it would be better to use variables to make the code easier to read. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done in |
||
} | ||
|
||
/** | ||
* Returns a NotFoundHttpException. | ||
* | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ | |
namespace Symfony\Bundle\FrameworkBundle; | ||
|
||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpFoundation\StreamedResponse; | ||
use Symfony\Component\HttpKernel\HttpKernelInterface; | ||
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; | ||
use Symfony\Component\DependencyInjection\ContainerInterface; | ||
|
@@ -146,7 +147,11 @@ public function render($controller, array $options = array()) | |
throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $request->getUri(), $response->getStatusCode())); | ||
} | ||
|
||
return $response->getContent(); | ||
if ($response instanceof StreamedResponse) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. imho it would be nicer to check the negation to return early and to get rid of the else There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. changed |
||
$response->sendContent(); | ||
} else { | ||
return $response->getContent(); | ||
} | ||
} catch (\Exception $e) { | ||
if ($options['alt']) { | ||
$alt = $options['alt']; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\HttpFoundation; | ||
|
||
/** | ||
* StreamedResponse represents a streamed HTTP response. | ||
* | ||
* A StreamedResponse uses a callback for its the content. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
* | ||
* The callback should use the standard PHP functions like echo | ||
* to stream the response back to the client. The flush() method | ||
* can also be used if needed. | ||
* | ||
* @see flush() | ||
* | ||
* @author Fabien Potencier <fabien@symfony.com> | ||
* | ||
* @api | ||
*/ | ||
class StreamedResponse extends Response | ||
{ | ||
protected $callback; | ||
protected $streamed; | ||
|
||
/** | ||
* Constructor. | ||
* | ||
* @param mixed $callback A valid PHP callback | ||
* @param integer $status The response status code | ||
* @param array $headers An array of response headers | ||
* | ||
* @api | ||
*/ | ||
public function __construct($callback, $status = 200, $headers = array()) | ||
{ | ||
parent::__construct(null, $status, $headers); | ||
|
||
$this->callback = $callback; | ||
$this->streamed = false; | ||
} | ||
|
||
/** | ||
* @{inheritdoc} | ||
*/ | ||
public function prepare(Request $request) | ||
{ | ||
if ('1.0' != $request->server->get('SERVER_PROTOCOL')) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be |
||
$this->setProtocolVersion('1.1'); | ||
$this->headers->set('Transfer-Encoding', 'chunked'); | ||
} | ||
|
||
$this->headers->set('Cache-Control', 'no-cache'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is probably a good reason to avoid caching but I can not figure out, any hint ? semantic: maybe this should be moved after the call to the parent method ? |
||
|
||
parent::prepare($request); | ||
} | ||
|
||
/** | ||
* @{inheritdoc} | ||
* | ||
* This method only sends the content once. | ||
*/ | ||
public function sendContent() | ||
{ | ||
if ($this->streamed) { | ||
return; | ||
} | ||
|
||
$this->streamed = true; | ||
|
||
if (!is_callable($this->callback)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this test be moved to the ctor ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. moved. |
||
throw new \LogicException('The Response callback is not a valid PHP callable.'); | ||
} | ||
|
||
call_user_func($this->callback); | ||
} | ||
|
||
/** | ||
* @{inheritdoc} | ||
* | ||
* @throws \LogicException when the content is not null | ||
*/ | ||
public function setContent($content) | ||
{ | ||
if (null !== $content) { | ||
throw new \LogicException('The content cannot be set on a StreamedResponse instance.'); | ||
} | ||
} | ||
|
||
/** | ||
* @{inheritdoc} | ||
* | ||
* @return false | ||
*/ | ||
public function getContent() | ||
{ | ||
return false; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\HttpKernel\EventListener; | ||
|
||
use Symfony\Component\HttpFoundation\StreamedResponse; | ||
use Symfony\Component\HttpKernel\Event\FilterResponseEvent; | ||
use Symfony\Component\HttpKernel\HttpKernelInterface; | ||
use Symfony\Component\HttpKernel\KernelEvents; | ||
use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||
|
||
/** | ||
* StreamedResponseListener is responsible for sending the Response | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. type: response There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think we do this in other places too when we want to refer to a class instance. |
||
* to the client. | ||
* | ||
* @author Fabien Potencier <fabien@symfony.com> | ||
*/ | ||
class StreamedResponseListener implements EventSubscriberInterface | ||
{ | ||
/** | ||
* Filters the Response. | ||
* | ||
* @param FilterResponseEvent $event A FilterResponseEvent instance | ||
*/ | ||
public function onKernelResponse(FilterResponseEvent $event) | ||
{ | ||
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { | ||
return; | ||
} | ||
|
||
$response = $event->getResponse(); | ||
|
||
if ($response instanceof StreamedResponse) { | ||
$response->send(); | ||
} | ||
} | ||
|
||
static public function getSubscribedEvents() | ||
{ | ||
return array( | ||
KernelEvents::RESPONSE => array('onKernelResponse', -1024), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need this listener at the end of the kernel.response event ? The event is the last step before the end of the request and There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It must be the last one as we are sending the content here. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you have this event listener at all actually? Can't you just let the send() method be called as usual by the front controller? Sounds like it's equivalent to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The event listener is needed because we need to be in the request scope to be able to generate the response. If you send the response in the front controller, the request is not available anymore. |
||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,6 +46,20 @@ interface EngineInterface | |
*/ | ||
function render($name, array $parameters = array()); | ||
|
||
/** | ||
* Streams a template. | ||
* | ||
* The implementation should outputs the content directly to the client. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo: output There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
* | ||
* @param mixed $name A template name or a TemplateReferenceInterface instance | ||
* @param array $parameters An array of parameters to pass to the template | ||
* | ||
* @throws \RuntimeException if the template cannot be rendered | ||
* | ||
* @api | ||
*/ | ||
function stream($name, array $parameters = array()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should be marked as a BC break as the interface was tagged with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or we instead create a new There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this would be difficult for the DelegatingEngine as it would need to implement the interface selectively depending of the template passed to the method :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can also make the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sounds good to me |
||
|
||
/** | ||
* Returns true if the template exists. | ||
* | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The render() call does accept a Response, and if you pass one it is reused, but in this case it is not reused it's replaced by a StreamingResponse. If anything this should only accept a StreamingResponse so that it can be reused, except it's not possible to set the callback later than in the constructor, so that does not make sense either. I would just remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is useful to set the status code or some headers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't agree. That can easily be done on the returned response. Allowing a response to passed while it's not going to be used (only read and then discarded) is misleading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @Seldaek. unless we think its a very common case that one has a Response coming from somewhere, i think it would be better to leave this feature out. even then we should rather add a method to the
StreamedResponse
class that populates a defined set of properties from a Response.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead, I have added the possibility to changed the
StreamedResponse
callback after its creation and I've forced the$response
argument to be an instance ofStreamedResponse
(see1d368e75fc473741b).