Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[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

Merged
merged 5 commits into from
Dec 31, 2011
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
[HttpFoundation] added support for streamed responses
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
fabpot committed Dec 21, 2011
commit 0038d1bac4a8d2dbe83f12b6a5236e8a2161b7d9
1 change: 1 addition & 0 deletions 1 CHANGELOG-2.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c

### HttpFoundation

* added support for streamed responses
* made Response::prepare() method the place to enforce HTTP specification
* [BC BREAK] moved management of the locale from the Session class to the Request class
* added a generic access to the PHP built-in filter mechanism: ParameterBag::filter()
Expand Down
19 changes: 19 additions & 0 deletions 19 src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Member Author

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 of StreamedResponse (see 1d368e75fc 473741b).

{
$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());
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no specific reason.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 1d368e75fc 473741b

}

/**
* Returns a NotFoundHttpException.
*
Expand Down
7 changes: 6 additions & 1 deletion 7 src/Symfony/Bundle/FrameworkBundle/HttpKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The 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'];
Expand Down
5 changes: 5 additions & 0 deletions 5 src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<parameter key="controller_resolver.class">Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver</parameter>
<parameter key="controller_name_converter.class">Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser</parameter>
<parameter key="response_listener.class">Symfony\Component\HttpKernel\EventListener\ResponseListener</parameter>
<parameter key="streamed_response_listener.class">Symfony\Component\HttpKernel\EventListener\StreamedResponseListener</parameter>
<parameter key="locale_listener.class">Symfony\Component\HttpKernel\EventListener\LocaleListener</parameter>
</parameters>

Expand All @@ -29,6 +30,10 @@
<argument>%kernel.charset%</argument>
</service>

<service id="streamed_response_listener" class="%streamed_response_listener.class%">
<tag name="kernel.event_subscriber" />
</service>

<service id="locale_listener" class="%locale_listener.class%">
<tag name="kernel.event_subscriber" />
<argument>%kernel.default_locale%</argument>
Expand Down
13 changes: 13 additions & 0 deletions 13 src/Symfony/Bundle/TwigBundle/TwigEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ public function render($name, array $parameters = array())
}
}

/**
* Streams a template.
*
* @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
*/
public function stream($name, array $parameters = array())
{
$this->load($name)->display($parameters);
}

/**
* Returns true if the template exists.
*
Expand Down
107 changes: 107 additions & 0 deletions 107 src/Symfony/Component/HttpFoundation/StreamedResponse.php
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: the

Copy link
Member Author

Choose a reason for hiding this comment

The 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')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be !== for CS no?

$this->setProtocolVersion('1.1');
$this->headers->set('Transfer-Encoding', 'chunked');
}

$this->headers->set('Cache-Control', 'no-cache');
Copy link
Contributor

Choose a reason for hiding this comment

The 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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this test be moved to the ctor ?

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type: response

Copy link
Contributor

Choose a reason for hiding this comment

The 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),
Copy link
Member

Choose a reason for hiding this comment

The 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 send will be called by the front controller just after that.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 send() in the front controller is a no-op for streamed responses.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

);
}
}
15 changes: 15 additions & 0 deletions 15 src/Symfony/Component/Templating/DelegatingEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ public function render($name, array $parameters = array())
return $this->getEngine($name)->render($name, $parameters);
}

/**
* Streams a template.
*
* @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
*/
public function stream($name, array $parameters = array())
{
$this->getEngine($name)->stream($name, $parameters);
}

/**
* Returns true if the template exists.
*
Expand Down
14 changes: 14 additions & 0 deletions 14 src/Symfony/Component/Templating/EngineInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: output

Copy link
Member Author

Choose a reason for hiding this comment

The 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());
Copy link
Member

Choose a reason for hiding this comment

The 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 @api

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or we instead create a new StreamingEngineInterface which imho is cleaner than the exception

Copy link
Member

Choose a reason for hiding this comment

The 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 :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also make the DelegatingEngine implement the StreamingEngineInterface and throw an exception if the engine does not implement this interface in the stream method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good to me


/**
* Returns true if the template exists.
*
Expand Down
15 changes: 15 additions & 0 deletions 15 src/Symfony/Component/Templating/PhpEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,21 @@ public function render($name, array $parameters = array())
return $content;
}

/**
* Streams a template.
*
* @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
*/
public function stream($name, array $parameters = array())
{
throw new \LogicException('The PHP engine does not support streaming.');
}

/**
* Returns true if the template exists.
*
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.