Skip to content

Commit 899e252

Browse files
committed
merged branch symfony/streaming (PR #2935)
Commits ------- 887c0e9 moved EngineInterface::stream() to a new StreamingEngineInterface to keep BC with 2.0 473741b added the possibility to change a StreamedResponse callback after its creation 8717d44 moved a test in the constructor e44b8ba made some cosmetic changes 0038d1b [HttpFoundation] added support for streamed responses Discussion ---------- [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. --------------------------------------------------------------------------- by Seldaek at 2011/12/21 06:34:13 -0800 Just an idea: what about exposing flush() to twig? Possibly in a way that it will not call it if the template is not streaming. That way you could always add a flush() after your </head> tag to make sure that goes out as fast as possible, but it wouldn't mess with non-streamed responses. Although it appears flush() doesn't affect output buffers, so I guess it doesn't need anything special. When you say "ESI is not supported.", that means only the AppCache right? I don't see why this would affect Varnish, but then again as far as I know Varnish will buffer if ESI is used so the benefit of streaming there is non-existent. --------------------------------------------------------------------------- by cordoval at 2011/12/21 08:04:21 -0800 wonder what the use case is for streaming a response, very interesting. --------------------------------------------------------------------------- by johnkary at 2011/12/21 08:19:48 -0800 @cordoval Common use cases are present fairly well by this RailsCast video: http://railscasts.com/episodes/266-http-streaming Essentially it allows faster fetching of web assets (JS, CSS, etc) located in the &lt;head>&lt;/head>, allowing those assets to be fetched as soon as possible before the remainder of the content body is computed and sent to the browser. The end goal is to improve page load speed. There are other uses cases too like making large body content available quickly to the service consuming it. Think if you were monitoring a live feed of JSON data of newest Twitter comments. --------------------------------------------------------------------------- by lsmith77 at 2011/12/21 08:54:35 -0800 How does this relate the limitations mentioned in: http://yehudakatz.com/2010/09/07/automatic-flushing-the-rails-3-1-plan/ Am I right to understand that due to how twig works we are not really streaming the content pieces when we call render(), but instead the entire template with its layout is rendered and only then will we flush? or does it mean that the render call will work its way to the top level layout template and form then on it can send the content until it hits another block, which it then first renders before it continues to send the data? --------------------------------------------------------------------------- by stof at 2011/12/21 09:02:53 -0800 @lsmith77 this is why the ``stream`` method calls ``display`` in Twig instead of ``render``. ``display`` uses echo to print the output of the template line by line (and blocks are simply method calls in the middle). Look at your compiled templates to see it (the ``doDisplay`` method) Rendering a template with Twig simply use an output buffer around the rendering. --------------------------------------------------------------------------- by fabpot at 2011/12/21 09:24:33 -0800 @lsmith77: We don't have the Rails problem thanks to Twig as the order of execution is the right one by default (the layout is executed first); it means that we can have the flush feature without any change to how the core works. As @stof mentioned, we are using `display`, not `render`, so we are streaming your templates for byte one. --------------------------------------------------------------------------- by fabpot at 2011/12/21 09:36:41 -0800 @Seldaek: yes, I meant ESI with the PHP reverse proxy. --------------------------------------------------------------------------- by fabpot at 2011/12/21 09:37:34 -0800 @Seldaek: I have `flush()` support for Twig on my todo-list. As you mentioned, It should be trivial to implement. --------------------------------------------------------------------------- by fzaninotto at 2011/12/21 09:48:18 -0800 How do streaming responses deal with assets that must be called in the head, but are declared in the body? --------------------------------------------------------------------------- by fabpot at 2011/12/21 09:52:12 -0800 @fzaninotto: What do you mean? With Twig, your layout is defined with blocks ("holes"). These blocks are overridden by child templates, but evaluated as they are encountered in the layout. So, everything works as expected. As noted in the commit message, this does not work with PHP templates for the problems mentioned in the Rails post (as the order of execution is not the right one -- the child template is first evaluated and then the layout). --------------------------------------------------------------------------- by fzaninotto at 2011/12/21 10:07:35 -0800 I was referring to using Assetic. Not sure if this compiles to Twig the same way as javascript and stylesheet blocks placed in the head - and therefore executed in the right way. --------------------------------------------------------------------------- by fabpot at 2011/12/21 10:34:59 -0800 @Seldaek: I've just added a `flush` tag in Twig 1.5: fabpot/Twig@1d6dfad --------------------------------------------------------------------------- by catchamonkey at 2011/12/21 13:29:22 -0800 I'm really happy you've got this into the core, it's a great feature to have! Good work.
2 parents d12f5b2 + 887c0e9 commit 899e252

File tree

12 files changed

+387
-4
lines changed

12 files changed

+387
-4
lines changed

CHANGELOG-2.1.md

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c
146146

147147
### HttpFoundation
148148

149+
* added support for streamed responses
149150
* made Response::prepare() method the place to enforce HTTP specification
150151
* [BC BREAK] moved management of the locale from the Session class to the Request class
151152
* added a generic access to the PHP built-in filter mechanism: ParameterBag::filter()

src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php

+27
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\HttpFoundation\Response;
1515
use Symfony\Component\HttpFoundation\RedirectResponse;
16+
use Symfony\Component\HttpFoundation\StreamedResponse;
1617
use Symfony\Component\DependencyInjection\ContainerAware;
1718
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
1819
use Symfony\Component\Form\FormTypeInterface;
@@ -98,6 +99,32 @@ public function render($view, array $parameters = array(), Response $response =
9899
return $this->container->get('templating')->renderResponse($view, $parameters, $response);
99100
}
100101

102+
/**
103+
* Streams a view.
104+
*
105+
* @param string $view The view name
106+
* @param array $parameters An array of parameters to pass to the view
107+
* @param StreamedResponse $response A response instance
108+
*
109+
* @return StreamedResponse A StreamedResponse instance
110+
*/
111+
public function stream($view, array $parameters = array(), StreamedResponse $response = null)
112+
{
113+
$templating = $this->container->get('templating');
114+
115+
$callback = function () use ($templating, $view, $parameters) {
116+
$templating->stream($view, $parameters);
117+
};
118+
119+
if (null === $response) {
120+
return new StreamedResponse($callback);
121+
}
122+
123+
$response->setCallback($callback);
124+
125+
return $response;
126+
}
127+
101128
/**
102129
* Returns a NotFoundHttpException.
103130
*

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

+1
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ function($v, Reference $ref) use ($container) {
389389
$this->addClassesToCompile(array(
390390
'Symfony\\Bundle\\FrameworkBundle\\Templating\\GlobalVariables',
391391
'Symfony\\Bundle\\FrameworkBundle\\Templating\\EngineInterface',
392+
'Symfony\\Bundle\\FrameworkBundle\\Templating\\StreamingEngineInterface',
392393
'Symfony\\Component\\Templating\\TemplateNameParserInterface',
393394
'Symfony\\Component\\Templating\\TemplateNameParser',
394395
'Symfony\\Component\\Templating\\EngineInterface',

src/Symfony/Bundle/FrameworkBundle/HttpKernel.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Bundle\FrameworkBundle;
1313

1414
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\StreamedResponse;
1516
use Symfony\Component\HttpKernel\HttpKernelInterface;
1617
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
1718
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -146,7 +147,11 @@ public function render($controller, array $options = array())
146147
throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $request->getUri(), $response->getStatusCode()));
147148
}
148149

149-
return $response->getContent();
150+
if (!$response instanceof StreamedResponse) {
151+
return $response->getContent();
152+
}
153+
154+
$response->sendContent();
150155
} catch (\Exception $e) {
151156
if ($options['alt']) {
152157
$alt = $options['alt'];

src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml

+5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<parameter key="controller_resolver.class">Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver</parameter>
99
<parameter key="controller_name_converter.class">Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser</parameter>
1010
<parameter key="response_listener.class">Symfony\Component\HttpKernel\EventListener\ResponseListener</parameter>
11+
<parameter key="streamed_response_listener.class">Symfony\Component\HttpKernel\EventListener\StreamedResponseListener</parameter>
1112
<parameter key="locale_listener.class">Symfony\Component\HttpKernel\EventListener\LocaleListener</parameter>
1213
</parameters>
1314

@@ -29,6 +30,10 @@
2930
<argument>%kernel.charset%</argument>
3031
</service>
3132

33+
<service id="streamed_response_listener" class="%streamed_response_listener.class%">
34+
<tag name="kernel.event_subscriber" />
35+
</service>
36+
3237
<service id="locale_listener" class="%locale_listener.class%">
3338
<tag name="kernel.event_subscriber" />
3439
<argument>%kernel.default_locale%</argument>

src/Symfony/Bundle/TwigBundle/TwigEngine.php

+15-1
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@
1717
use Symfony\Component\Templating\TemplateNameParserInterface;
1818
use Symfony\Component\HttpFoundation\Response;
1919
use Symfony\Component\Config\FileLocatorInterface;
20+
use Symfony\Component\Templating\StreamingEngineInterface;
2021

2122
/**
2223
* This engine knows how to render Twig templates.
2324
*
2425
* @author Fabien Potencier <fabien@symfony.com>
2526
*/
26-
class TwigEngine implements EngineInterface
27+
class TwigEngine implements EngineInterface, StreamingEngineInterface
2728
{
2829
protected $environment;
2930
protected $parser;
@@ -75,6 +76,19 @@ public function render($name, array $parameters = array())
7576
}
7677
}
7778

79+
/**
80+
* Streams a template.
81+
*
82+
* @param mixed $name A template name or a TemplateReferenceInterface instance
83+
* @param array $parameters An array of parameters to pass to the template
84+
*
85+
* @throws \RuntimeException if the template cannot be rendered
86+
*/
87+
public function stream($name, array $parameters = array())
88+
{
89+
$this->load($name)->display($parameters);
90+
}
91+
7892
/**
7993
* Returns true if the template exists.
8094
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation;
13+
14+
/**
15+
* StreamedResponse represents a streamed HTTP response.
16+
*
17+
* A StreamedResponse uses a callback for its content.
18+
*
19+
* The callback should use the standard PHP functions like echo
20+
* to stream the response back to the client. The flush() method
21+
* can also be used if needed.
22+
*
23+
* @see flush()
24+
*
25+
* @author Fabien Potencier <fabien@symfony.com>
26+
*
27+
* @api
28+
*/
29+
class StreamedResponse extends Response
30+
{
31+
protected $callback;
32+
protected $streamed;
33+
34+
/**
35+
* Constructor.
36+
*
37+
* @param mixed $callback A valid PHP callback
38+
* @param integer $status The response status code
39+
* @param array $headers An array of response headers
40+
*
41+
* @api
42+
*/
43+
public function __construct($callback = null, $status = 200, $headers = array())
44+
{
45+
parent::__construct(null, $status, $headers);
46+
47+
if (null !== $callback) {
48+
$this->setCallback($callback);
49+
}
50+
$this->streamed = false;
51+
}
52+
53+
/**
54+
* Sets the PHP callback associated with this Response.
55+
*
56+
* @param mixed $callback A valid PHP callback
57+
*/
58+
public function setCallback($callback)
59+
{
60+
$this->callback = $callback;
61+
if (!is_callable($this->callback)) {
62+
throw new \LogicException('The Response callback must be a valid PHP callable.');
63+
}
64+
}
65+
66+
/**
67+
* @{inheritdoc}
68+
*/
69+
public function prepare(Request $request)
70+
{
71+
if ('1.0' != $request->server->get('SERVER_PROTOCOL')) {
72+
$this->setProtocolVersion('1.1');
73+
$this->headers->set('Transfer-Encoding', 'chunked');
74+
}
75+
76+
$this->headers->set('Cache-Control', 'no-cache');
77+
78+
parent::prepare($request);
79+
}
80+
81+
/**
82+
* @{inheritdoc}
83+
*
84+
* This method only sends the content once.
85+
*/
86+
public function sendContent()
87+
{
88+
if ($this->streamed) {
89+
return;
90+
}
91+
92+
$this->streamed = true;
93+
94+
if (null === $this->callback) {
95+
throw new \LogicException('The Response callback must not be null.');
96+
}
97+
98+
call_user_func($this->callback);
99+
}
100+
101+
/**
102+
* @{inheritdoc}
103+
*
104+
* @throws \LogicException when the content is not null
105+
*/
106+
public function setContent($content)
107+
{
108+
if (null !== $content) {
109+
throw new \LogicException('The content cannot be set on a StreamedResponse instance.');
110+
}
111+
}
112+
113+
/**
114+
* @{inheritdoc}
115+
*
116+
* @return false
117+
*/
118+
public function getContent()
119+
{
120+
return false;
121+
}
122+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\EventListener;
13+
14+
use Symfony\Component\HttpFoundation\StreamedResponse;
15+
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
16+
use Symfony\Component\HttpKernel\HttpKernelInterface;
17+
use Symfony\Component\HttpKernel\KernelEvents;
18+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
19+
20+
/**
21+
* StreamedResponseListener is responsible for sending the Response
22+
* to the client.
23+
*
24+
* @author Fabien Potencier <fabien@symfony.com>
25+
*/
26+
class StreamedResponseListener implements EventSubscriberInterface
27+
{
28+
/**
29+
* Filters the Response.
30+
*
31+
* @param FilterResponseEvent $event A FilterResponseEvent instance
32+
*/
33+
public function onKernelResponse(FilterResponseEvent $event)
34+
{
35+
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
36+
return;
37+
}
38+
39+
$response = $event->getResponse();
40+
41+
if ($response instanceof StreamedResponse) {
42+
$response->send();
43+
}
44+
}
45+
46+
static public function getSubscribedEvents()
47+
{
48+
return array(
49+
KernelEvents::RESPONSE => array('onKernelResponse', -1024),
50+
);
51+
}
52+
}

src/Symfony/Component/Templating/DelegatingEngine.php

+21-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*
1919
* @api
2020
*/
21-
class DelegatingEngine implements EngineInterface
21+
class DelegatingEngine implements EngineInterface, StreamingEngineInterface
2222
{
2323
protected $engines;
2424

@@ -55,6 +55,26 @@ public function render($name, array $parameters = array())
5555
return $this->getEngine($name)->render($name, $parameters);
5656
}
5757

58+
/**
59+
* Streams a template.
60+
*
61+
* @param mixed $name A template name or a TemplateReferenceInterface instance
62+
* @param array $parameters An array of parameters to pass to the template
63+
*
64+
* @throws \RuntimeException if the template cannot be rendered
65+
*
66+
* @api
67+
*/
68+
public function stream($name, array $parameters = array())
69+
{
70+
$engine = $this->getEngine($name);
71+
if (!$engine instanceof StreamingEngineInterface) {
72+
throw new \LogicException(sprintf('Template "%s" cannot be streamed as the engine supporting it does not implement StreamingEngineInterface.', $name));
73+
}
74+
75+
$engine->stream($name, $parameters);
76+
}
77+
5878
/**
5979
* Returns true if the template exists.
6080
*

src/Symfony/Component/Templating/PhpEngine.php

+16-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
*
2929
* @api
3030
*/
31-
class PhpEngine implements EngineInterface, \ArrayAccess
31+
class PhpEngine implements EngineInterface, StreamingEngineInterface, \ArrayAccess
3232
{
3333
protected $loader;
3434
protected $current;
@@ -107,6 +107,21 @@ public function render($name, array $parameters = array())
107107
return $content;
108108
}
109109

110+
/**
111+
* Streams a template.
112+
*
113+
* @param mixed $name A template name or a TemplateReferenceInterface instance
114+
* @param array $parameters An array of parameters to pass to the template
115+
*
116+
* @throws \RuntimeException if the template cannot be rendered
117+
*
118+
* @api
119+
*/
120+
public function stream($name, array $parameters = array())
121+
{
122+
throw new \LogicException('The PHP engine does not support streaming.');
123+
}
124+
110125
/**
111126
* Returns true if the template exists.
112127
*

0 commit comments

Comments
 (0)