Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

[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...
commit 0038d1bac4a8d2dbe83f12b6a5236e8a2161b7d9 1 parent cc8f308
@fabpot fabpot authored
View
1  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()
View
19 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;
@@ -99,6 +100,24 @@ 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
+ *
+ * @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.
*
* This will result in a 404 response code. Usage example:
View
7 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'];
View
5 src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml
@@ -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>
@@ -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>
View
13 src/Symfony/Bundle/TwigBundle/TwigEngine.php
@@ -76,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.
*
* @param mixed $name A template name
View
107 src/Symfony/Component/HttpFoundation/StreamedResponse.php
@@ -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.
+ *
+ * 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')) {
+ $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;
+ }
+}
View
52 src/Symfony/Component/HttpKernel/EventListener/StreamedResponseListener.php
@@ -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
+ * 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),
+ );
+ }
+}
View
15 src/Symfony/Component/Templating/DelegatingEngine.php
@@ -56,6 +56,21 @@ 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
+ *
+ * @api
+ */
+ public function stream($name, array $parameters = array())
+ {
+ $this->getEngine($name)->stream($name, $parameters);
+ }
+
+ /**
* Returns true if the template exists.
*
* @param mixed $name A template name or a TemplateReferenceInterface instance
View
14 src/Symfony/Component/Templating/EngineInterface.php
@@ -47,6 +47,20 @@
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.
*
* @param mixed $name A template name or a TemplateReferenceInterface instance
View
15 src/Symfony/Component/Templating/PhpEngine.php
@@ -108,6 +108,21 @@ 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
+ *
+ * @api
+ */
+ public function stream($name, array $parameters = array())
+ {
+ throw new \LogicException('The PHP engine does not support streaming.');
+ }
+
+ /**
* Returns true if the template exists.
*
* @param mixed $name A template name or a TemplateReferenceInterface instance
View
89 tests/Symfony/Tests/Component/HttpFoundation/StreamedResponseTest.php
@@ -0,0 +1,89 @@
+<?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\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());
+ }
+}
Please sign in to comment.
Something went wrong with that request. Please try again.