From 0038d1bac4a8d2dbe83f12b6a5236e8a2161b7d9 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 17 Oct 2011 17:17:34 +0200 Subject: [PATCH 1/5] [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. --- CHANGELOG-2.1.md | 1 + .../FrameworkBundle/Controller/Controller.php | 19 ++++ .../Bundle/FrameworkBundle/HttpKernel.php | 7 +- .../FrameworkBundle/Resources/config/web.xml | 5 + src/Symfony/Bundle/TwigBundle/TwigEngine.php | 13 +++ .../HttpFoundation/StreamedResponse.php | 107 ++++++++++++++++++ .../StreamedResponseListener.php | 52 +++++++++ .../Component/Templating/DelegatingEngine.php | 15 +++ .../Component/Templating/EngineInterface.php | 14 +++ .../Component/Templating/PhpEngine.php | 15 +++ .../HttpFoundation/StreamedResponseTest.php | 89 +++++++++++++++ 11 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/HttpFoundation/StreamedResponse.php create mode 100644 src/Symfony/Component/HttpKernel/EventListener/StreamedResponseListener.php create mode 100644 tests/Symfony/Tests/Component/HttpFoundation/StreamedResponseTest.php diff --git a/CHANGELOG-2.1.md b/CHANGELOG-2.1.md index e72611d7bf78d..c656c53ab14a2 100644 --- a/CHANGELOG-2.1.md +++ b/CHANGELOG-2.1.md @@ -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() diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php index 43ec0d1180252..20a102973e5c3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php @@ -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()); + } + /** * Returns a NotFoundHttpException. * diff --git a/src/Symfony/Bundle/FrameworkBundle/HttpKernel.php b/src/Symfony/Bundle/FrameworkBundle/HttpKernel.php index 60313723b6a45..6a3fc7f37ecbe 100644 --- a/src/Symfony/Bundle/FrameworkBundle/HttpKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/HttpKernel.php @@ -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) { + $response->sendContent(); + } else { + return $response->getContent(); + } } catch (\Exception $e) { if ($options['alt']) { $alt = $options['alt']; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml index b51e20fd1e42d..d34ecdf3c3727 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml @@ -8,6 +8,7 @@ Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser Symfony\Component\HttpKernel\EventListener\ResponseListener + Symfony\Component\HttpKernel\EventListener\StreamedResponseListener Symfony\Component\HttpKernel\EventListener\LocaleListener @@ -29,6 +30,10 @@ %kernel.charset% + + + + %kernel.default_locale% diff --git a/src/Symfony/Bundle/TwigBundle/TwigEngine.php b/src/Symfony/Bundle/TwigBundle/TwigEngine.php index 283cb8dfd9e3b..aa3086a8222fd 100644 --- a/src/Symfony/Bundle/TwigBundle/TwigEngine.php +++ b/src/Symfony/Bundle/TwigBundle/TwigEngine.php @@ -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. * diff --git a/src/Symfony/Component/HttpFoundation/StreamedResponse.php b/src/Symfony/Component/HttpFoundation/StreamedResponse.php new file mode 100644 index 0000000000000..bf059d715361f --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/StreamedResponse.php @@ -0,0 +1,107 @@ + + * + * 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. + * + * 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 + * + * @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')) { + $this->setProtocolVersion('1.1'); + $this->headers->set('Transfer-Encoding', 'chunked'); + } + + $this->headers->set('Cache-Control', 'no-cache'); + + 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)) { + 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; + } +} diff --git a/src/Symfony/Component/HttpKernel/EventListener/StreamedResponseListener.php b/src/Symfony/Component/HttpKernel/EventListener/StreamedResponseListener.php new file mode 100644 index 0000000000000..588c5fe9b1611 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/EventListener/StreamedResponseListener.php @@ -0,0 +1,52 @@ + + * + * 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 + * to the client. + * + * @author Fabien Potencier + */ +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), + ); + } +} diff --git a/src/Symfony/Component/Templating/DelegatingEngine.php b/src/Symfony/Component/Templating/DelegatingEngine.php index db7782736af6a..eae170f08123c 100644 --- a/src/Symfony/Component/Templating/DelegatingEngine.php +++ b/src/Symfony/Component/Templating/DelegatingEngine.php @@ -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. * diff --git a/src/Symfony/Component/Templating/EngineInterface.php b/src/Symfony/Component/Templating/EngineInterface.php index 3f3e29c3795bb..fc30c90406e21 100644 --- a/src/Symfony/Component/Templating/EngineInterface.php +++ b/src/Symfony/Component/Templating/EngineInterface.php @@ -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. + * + * @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()); + /** * Returns true if the template exists. * diff --git a/src/Symfony/Component/Templating/PhpEngine.php b/src/Symfony/Component/Templating/PhpEngine.php index 73be8ed3561c7..ed4485bed38b5 100644 --- a/src/Symfony/Component/Templating/PhpEngine.php +++ b/src/Symfony/Component/Templating/PhpEngine.php @@ -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. * diff --git a/tests/Symfony/Tests/Component/HttpFoundation/StreamedResponseTest.php b/tests/Symfony/Tests/Component/HttpFoundation/StreamedResponseTest.php new file mode 100644 index 0000000000000..50cc9db079df1 --- /dev/null +++ b/tests/Symfony/Tests/Component/HttpFoundation/StreamedResponseTest.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\StreamedResponse; + +class StreamedResponseTest extends \PHPUnit_Framework_TestCase +{ + public function testConstructor() + { + $response = new StreamedResponse(function () { echo 'foo'; }, 404, array('Content-Type' => 'text/plain')); + + $this->assertEquals(404, $response->getStatusCode()); + $this->assertEquals('text/plain', $response->headers->get('Content-Type')); + } + + public function testPrepareWith11Protocol() + { + $response = new StreamedResponse(function () { echo 'foo'; }); + $request = Request::create('/'); + $request->server->set('SERVER_PROTOCOL', '1.1'); + + $response->prepare($request); + + $this->assertEquals('1.1', $response->getProtocolVersion()); + $this->assertEquals('chunked', $response->headers->get('Transfer-Encoding')); + $this->assertEquals('no-cache, private', $response->headers->get('Cache-Control')); + } + + public function testPrepareWith10Protocol() + { + $response = new StreamedResponse(function () { echo 'foo'; }); + $request = Request::create('/'); + $request->server->set('SERVER_PROTOCOL', '1.0'); + + $response->prepare($request); + + $this->assertEquals('1.0', $response->getProtocolVersion()); + $this->assertNull($response->headers->get('Transfer-Encoding')); + $this->assertEquals('no-cache, private', $response->headers->get('Cache-Control')); + } + + public function testSendContent() + { + $called = 0; + + $response = new StreamedResponse(function () use (&$called) { ++$called; }); + + $response->sendContent(); + $this->assertEquals(1, $called); + + $response->sendContent(); + $this->assertEquals(1, $called); + } + + /** + * @expectedException \LogicException + */ + public function testSendContentWithNonCallable() + { + $response = new StreamedResponse('foobar'); + $response->sendContent(); + } + + /** + * @expectedException \LogicException + */ + public function testSetContent() + { + $response = new StreamedResponse(function () { echo 'foo'; }); + $response->setContent('foo'); + } + + public function testGetContent() + { + $response = new StreamedResponse(function () { echo 'foo'; }); + $this->assertEquals(false, $response->getContent()); + } +} From e44b8ba521e60f4b0c8218a65415c001090520da Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 21 Dec 2011 18:34:44 +0100 Subject: [PATCH 2/5] made some cosmetic changes --- src/Symfony/Bundle/FrameworkBundle/HttpKernel.php | 6 +++--- src/Symfony/Component/HttpFoundation/StreamedResponse.php | 2 +- src/Symfony/Component/Templating/EngineInterface.php | 2 +- .../Tests/Component/HttpFoundation/StreamedResponseTest.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/HttpKernel.php b/src/Symfony/Bundle/FrameworkBundle/HttpKernel.php index 6a3fc7f37ecbe..1c8fffca677c4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/HttpKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/HttpKernel.php @@ -147,11 +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())); } - if ($response instanceof StreamedResponse) { - $response->sendContent(); - } else { + if (!$response instanceof StreamedResponse) { return $response->getContent(); } + + $response->sendContent(); } catch (\Exception $e) { if ($options['alt']) { $alt = $options['alt']; diff --git a/src/Symfony/Component/HttpFoundation/StreamedResponse.php b/src/Symfony/Component/HttpFoundation/StreamedResponse.php index bf059d715361f..a5df4f7df95cb 100644 --- a/src/Symfony/Component/HttpFoundation/StreamedResponse.php +++ b/src/Symfony/Component/HttpFoundation/StreamedResponse.php @@ -14,7 +14,7 @@ /** * StreamedResponse represents a streamed HTTP response. * - * A StreamedResponse uses a callback for its the content. + * 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 diff --git a/src/Symfony/Component/Templating/EngineInterface.php b/src/Symfony/Component/Templating/EngineInterface.php index fc30c90406e21..040e346ee672b 100644 --- a/src/Symfony/Component/Templating/EngineInterface.php +++ b/src/Symfony/Component/Templating/EngineInterface.php @@ -49,7 +49,7 @@ function render($name, array $parameters = array()); /** * Streams a template. * - * The implementation should outputs the content directly to the client. + * The implementation should output the content directly to the client. * * @param mixed $name A template name or a TemplateReferenceInterface instance * @param array $parameters An array of parameters to pass to the template diff --git a/tests/Symfony/Tests/Component/HttpFoundation/StreamedResponseTest.php b/tests/Symfony/Tests/Component/HttpFoundation/StreamedResponseTest.php index 50cc9db079df1..308a4651fb528 100644 --- a/tests/Symfony/Tests/Component/HttpFoundation/StreamedResponseTest.php +++ b/tests/Symfony/Tests/Component/HttpFoundation/StreamedResponseTest.php @@ -84,6 +84,6 @@ public function testSetContent() public function testGetContent() { $response = new StreamedResponse(function () { echo 'foo'; }); - $this->assertEquals(false, $response->getContent()); + $this->assertFalse($response->getContent()); } } From 8717d4425e68949efc7b4c3c0ead69345b6fd097 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 21 Dec 2011 18:53:18 +0100 Subject: [PATCH 3/5] moved a test in the constructor --- src/Symfony/Component/HttpFoundation/StreamedResponse.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpFoundation/StreamedResponse.php b/src/Symfony/Component/HttpFoundation/StreamedResponse.php index a5df4f7df95cb..48aec2476da63 100644 --- a/src/Symfony/Component/HttpFoundation/StreamedResponse.php +++ b/src/Symfony/Component/HttpFoundation/StreamedResponse.php @@ -45,6 +45,10 @@ public function __construct($callback, $status = 200, $headers = array()) parent::__construct(null, $status, $headers); $this->callback = $callback; + if (!is_callable($this->callback)) { + throw new \LogicException('The Response callback must be a valid PHP callable.'); + } + $this->streamed = false; } @@ -76,10 +80,6 @@ public function sendContent() $this->streamed = true; - if (!is_callable($this->callback)) { - throw new \LogicException('The Response callback is not a valid PHP callable.'); - } - call_user_func($this->callback); } From 473741b9db7a2868c19033ff62963fd1b37ea29e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 22 Dec 2011 07:40:06 +0100 Subject: [PATCH 4/5] added the possibility to change a StreamedResponse callback after its creation --- .../FrameworkBundle/Controller/Controller.php | 20 ++++++++++++------ .../HttpFoundation/StreamedResponse.php | 21 ++++++++++++++++--- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php index 20a102973e5c3..24304f00c5bdf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php @@ -102,19 +102,27 @@ public function render($view, array $parameters = array(), Response $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 + * @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(), Response $response = null) + public function stream($view, array $parameters = array(), StreamedResponse $response = null) { $templating = $this->container->get('templating'); - return new StreamedResponse(function () use ($templating, $view, $parameters) { + $callback = function () use ($templating, $view, $parameters) { $templating->stream($view, $parameters); - }, null === $response ? 200 : $response->getStatusCode(), null === $response ? array() : $response->headers->all()); + }; + + if (null === $response) { + return new StreamedResponse($callback); + } + + $response->setCallback($callback); + + return $response; } /** diff --git a/src/Symfony/Component/HttpFoundation/StreamedResponse.php b/src/Symfony/Component/HttpFoundation/StreamedResponse.php index 48aec2476da63..503ccef7831d2 100644 --- a/src/Symfony/Component/HttpFoundation/StreamedResponse.php +++ b/src/Symfony/Component/HttpFoundation/StreamedResponse.php @@ -40,16 +40,27 @@ class StreamedResponse extends Response * * @api */ - public function __construct($callback, $status = 200, $headers = array()) + 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.'); } - - $this->streamed = false; } /** @@ -80,6 +91,10 @@ public function sendContent() $this->streamed = true; + if (null === $this->callback) { + throw new \LogicException('The Response callback must not be null.'); + } + call_user_func($this->callback); } From 887c0e9c04af4219ce5d4aa536e2e797614e94d2 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 31 Dec 2011 08:07:23 +0100 Subject: [PATCH 5/5] moved EngineInterface::stream() to a new StreamingEngineInterface to keep BC with 2.0 --- .../FrameworkExtension.php | 1 + src/Symfony/Bundle/TwigBundle/TwigEngine.php | 3 +- .../Component/Templating/DelegatingEngine.php | 9 ++++-- .../Component/Templating/EngineInterface.php | 14 -------- .../Component/Templating/PhpEngine.php | 2 +- .../Templating/StreamingEngineInterface.php | 32 +++++++++++++++++++ 6 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 src/Symfony/Component/Templating/StreamingEngineInterface.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index e421ab0f695e4..6e16fb51cd0b2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -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', diff --git a/src/Symfony/Bundle/TwigBundle/TwigEngine.php b/src/Symfony/Bundle/TwigBundle/TwigEngine.php index aa3086a8222fd..2ca6c5c7be539 100644 --- a/src/Symfony/Bundle/TwigBundle/TwigEngine.php +++ b/src/Symfony/Bundle/TwigBundle/TwigEngine.php @@ -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 */ -class TwigEngine implements EngineInterface +class TwigEngine implements EngineInterface, StreamingEngineInterface { protected $environment; protected $parser; diff --git a/src/Symfony/Component/Templating/DelegatingEngine.php b/src/Symfony/Component/Templating/DelegatingEngine.php index eae170f08123c..889da6fc54319 100644 --- a/src/Symfony/Component/Templating/DelegatingEngine.php +++ b/src/Symfony/Component/Templating/DelegatingEngine.php @@ -18,7 +18,7 @@ * * @api */ -class DelegatingEngine implements EngineInterface +class DelegatingEngine implements EngineInterface, StreamingEngineInterface { protected $engines; @@ -67,7 +67,12 @@ public function render($name, array $parameters = array()) */ public function stream($name, array $parameters = array()) { - $this->getEngine($name)->stream($name, $parameters); + $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); } /** diff --git a/src/Symfony/Component/Templating/EngineInterface.php b/src/Symfony/Component/Templating/EngineInterface.php index 040e346ee672b..3f3e29c3795bb 100644 --- a/src/Symfony/Component/Templating/EngineInterface.php +++ b/src/Symfony/Component/Templating/EngineInterface.php @@ -46,20 +46,6 @@ interface EngineInterface */ function render($name, array $parameters = array()); - /** - * Streams a template. - * - * The implementation should output the content directly to the client. - * - * @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()); - /** * Returns true if the template exists. * diff --git a/src/Symfony/Component/Templating/PhpEngine.php b/src/Symfony/Component/Templating/PhpEngine.php index ed4485bed38b5..4b23ad669bb88 100644 --- a/src/Symfony/Component/Templating/PhpEngine.php +++ b/src/Symfony/Component/Templating/PhpEngine.php @@ -28,7 +28,7 @@ * * @api */ -class PhpEngine implements EngineInterface, \ArrayAccess +class PhpEngine implements EngineInterface, StreamingEngineInterface, \ArrayAccess { protected $loader; protected $current; diff --git a/src/Symfony/Component/Templating/StreamingEngineInterface.php b/src/Symfony/Component/Templating/StreamingEngineInterface.php new file mode 100644 index 0000000000000..4b6b303bd3265 --- /dev/null +++ b/src/Symfony/Component/Templating/StreamingEngineInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Templating; + +/** + * StreamingEngineInterface provides a method that knows how to stream a template. + * + * @author Fabien Potencier + */ +interface StreamingEngineInterface +{ + /** + * Streams a template. + * + * The implementation should output the content directly to the client. + * + * @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 + */ + function stream($name, array $parameters = array()); +}