Skip to content

[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 all commits
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
1 change: 1 addition & 0 deletions 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
27 changes: 27 additions & 0 deletions 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,32 @@ 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 StreamedResponse $response A response instance
*
* @return StreamedResponse A StreamedResponse instance
*/
public function stream($view, array $parameters = array(), StreamedResponse $response = null)
{
$templating = $this->container->get('templating');

$callback = function () use ($templating, $view, $parameters) {
$templating->stream($view, $parameters);
};

if (null === $response) {
return new StreamedResponse($callback);
}

$response->setCallback($callback);

return $response;
}

/**
* Returns a NotFoundHttpException.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ function($v, Reference $ref) use ($container) {
$this->addClassesToCompile(array(
'Symfony\\Bundle\\FrameworkBundle\\Templating\\GlobalVariables',
'Symfony\\Bundle\\FrameworkBundle\\Templating\\EngineInterface',
'Symfony\\Bundle\\FrameworkBundle\\Templating\\StreamingEngineInterface',
'Symfony\\Component\\Templating\\TemplateNameParserInterface',
'Symfony\\Component\\Templating\\TemplateNameParser',
'Symfony\\Component\\Templating\\EngineInterface',
Expand Down
7 changes: 6 additions & 1 deletion 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) {
return $response->getContent();
}

$response->sendContent();
} catch (\Exception $e) {
if ($options['alt']) {
$alt = $options['alt'];
Expand Down
5 changes: 5 additions & 0 deletions 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
16 changes: 15 additions & 1 deletion src/Symfony/Bundle/TwigBundle/TwigEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@
use Symfony\Component\Templating\TemplateNameParserInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Templating\StreamingEngineInterface;

/**
* This engine knows how to render Twig templates.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class TwigEngine implements EngineInterface
class TwigEngine implements EngineInterface, StreamingEngineInterface
{
protected $environment;
protected $parser;
Expand Down Expand Up @@ -75,6 +76,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
122 changes: 122 additions & 0 deletions src/Symfony/Component/HttpFoundation/StreamedResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?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 content.
*
* 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 = null, $status = 200, $headers = array())
{
parent::__construct(null, $status, $headers);

if (null !== $callback) {
$this->setCallback($callback);
}
$this->streamed = false;
}

/**
* Sets the PHP callback associated with this Response.
*
* @param mixed $callback A valid PHP callback
*/
public function setCallback($callback)
{
$this->callback = $callback;
if (!is_callable($this->callback)) {
throw new \LogicException('The Response callback must be a valid PHP callable.');
}
}

/**
* @{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 (null === $this->callback) {
throw new \LogicException('The Response callback must not be null.');
}

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.

);
}
}
22 changes: 21 additions & 1 deletion src/Symfony/Component/Templating/DelegatingEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*
* @api
*/
class DelegatingEngine implements EngineInterface
class DelegatingEngine implements EngineInterface, StreamingEngineInterface
{
protected $engines;

Expand Down Expand Up @@ -55,6 +55,26 @@ 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())
{
$engine = $this->getEngine($name);
if (!$engine instanceof StreamingEngineInterface) {
throw new \LogicException(sprintf('Template "%s" cannot be streamed as the engine supporting it does not implement StreamingEngineInterface.', $name));
}

$engine->stream($name, $parameters);
}

/**
* Returns true if the template exists.
*
Expand Down
17 changes: 16 additions & 1 deletion src/Symfony/Component/Templating/PhpEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
*
* @api
*/
class PhpEngine implements EngineInterface, \ArrayAccess
class PhpEngine implements EngineInterface, StreamingEngineInterface, \ArrayAccess
Copy link

Choose a reason for hiding this comment

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

Is there a reason why the PhpEngine implements the StreamingEngineInterface just to throw an Exception? Didn't you say that streaming responses are impossible for PHP templates by design?

Copy link
Member Author

Choose a reason for hiding this comment

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

You are right. This is a nice side-effect of having another interface for streaming. Fixed here: e462f7b

{
protected $loader;
protected $current;
Expand Down 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