diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
index f4359f0285..0b97d8dfa5 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
@@ -31,6 +31,7 @@ class HttpCache implements HttpKernelInterface
protected $store;
protected $request;
protected $esi;
+ protected $esiTtls;
/**
* Constructor.
@@ -136,6 +137,7 @@ class HttpCache implements HttpKernelInterface
if (HttpKernelInterface::MASTER_REQUEST === $type) {
$this->traces = array();
$this->request = $request;
+ $this->esiTtls = array();
}
$path = $request->getPathInfo();
@@ -160,9 +162,43 @@ class HttpCache implements HttpKernelInterface
$response->headers->set('X-Symfony-Cache', $this->getLog());
}
+ if (null !== $this->esi) {
+ $this->addEsiTtl($response);
+
+ if ($request === $this->request) {
+ $this->updateResponseCacheControl($response);
+ }
+ }
+
return $response;
}
+ /**
+ * Stores the response's TTL locally.
+ *
+ * @param Response $response
+ */
+ protected function addEsiTtl(Response $response)
+ {
+ $this->esiTtls[] = $response->isValidateable() ? 0 : $response->getTtl();
+ }
+
+ /**
+ * Changes the master response TTL to the smallest TTL received or force validation if
+ * one of the ESI has validation cache strategy.
+ *
+ * @param Response $response
+ */
+ protected function updateResponseCacheControl(Response $response)
+ {
+ $ttl = min($this->esiTtls);
+ if (0 === $ttl) {
+ $response->headers->set('Cache-Control', 'no-cache, must-revalidate');
+ } else {
+ $response->setSharedMaxAge($ttl);
+ }
+ }
+
/**
* Forwards the Request to the backend without storing the Response in the cache.
*
diff --git a/tests/Symfony/Tests/Component/HttpKernel/HttpCache/HttpCacheTest.php b/tests/Symfony/Tests/Component/HttpKernel/HttpCache/HttpCacheTest.php
index 4c0d7cfde9..d6a5ba7f2a 100644
--- a/tests/Symfony/Tests/Component/HttpKernel/HttpCache/HttpCacheTest.php
+++ b/tests/Symfony/Tests/Component/HttpKernel/HttpCache/HttpCacheTest.php
@@ -893,4 +893,67 @@ class HttpCacheTest extends HttpCacheTestCase
$this->assertExceptionsAreNotCaught();
}
+
+ public function testEsiCacheSendsTheLowestTtl()
+ {
+ $responses = array(
+ array(
+ 'status' => 200,
+ 'body' => ' ',
+ 'headers' => array(
+ 'Cache-Control' => 's-maxage=300',
+ 'Surrogate-Control' => 'content="ESI/1.0"',
+ ),
+ ),
+ array(
+ 'status' => 200,
+ 'body' => 'Hello World!',
+ 'headers' => array('Cache-Control' => 's-maxage=300'),
+ ),
+ array(
+ 'status' => 200,
+ 'body' => 'My name is Bobby.',
+ 'headers' => array('Cache-Control' => 's-maxage=100'),
+ ),
+ );
+
+ $this->setNextResponses($responses);
+
+ $this->request('GET', '/', array(), array(), true);
+ $this->assertEquals("Hello World! My name is Bobby.", $this->response->getContent());
+ $this->assertEquals(100, $this->response->getTtl());
+ }
+
+ public function testEsiCacheForceValidation()
+ {
+ $responses = array(
+ array(
+ 'status' => 200,
+ 'body' => ' ',
+ 'headers' => array(
+ 'Cache-Control' => 's-maxage=300',
+ 'Surrogate-Control' => 'content="ESI/1.0"',
+ ),
+ ),
+ array(
+ 'status' => 200,
+ 'body' => 'Hello World!',
+ 'headers' => array('ETag' => 'foobar'),
+ ),
+ array(
+ 'status' => 200,
+ 'body' => 'My name is Bobby.',
+ 'headers' => array('Cache-Control' => 's-maxage=100'),
+ ),
+ );
+
+ $this->setNextResponses($responses);
+
+ $this->request('GET', '/', array(), array(), true);
+ $this->assertEquals('Hello World! My name is Bobby.', $this->response->getContent());
+ $this->assertEquals(null, $this->response->getTtl());
+ $this->assertTrue($this->response->mustRevalidate());
+ $this->assertTrue($this->response->headers->hasCacheControlDirective('private'));
+ $this->assertTrue($this->response->headers->hasCacheControlDirective('no-cache'));
+ }
}
diff --git a/tests/Symfony/Tests/Component/HttpKernel/HttpCache/HttpCacheTestCase.php b/tests/Symfony/Tests/Component/HttpKernel/HttpCache/HttpCacheTestCase.php
index 1af5811cc1..a7654a5e16 100644
--- a/tests/Symfony/Tests/Component/HttpKernel/HttpCache/HttpCacheTestCase.php
+++ b/tests/Symfony/Tests/Component/HttpKernel/HttpCache/HttpCacheTestCase.php
@@ -12,8 +12,10 @@
namespace Symfony\Tests\Component\HttpKernel\HttpCache;
require_once __DIR__.'/TestHttpKernel.php';
+require_once __DIR__.'/TestMultipleHttpKernel.php';
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\HttpCache\Esi;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
use Symfony\Component\HttpKernel\HttpCache\Store;
use Symfony\Component\HttpKernel\HttpKernelInterface;
@@ -28,12 +30,14 @@ class HttpCacheTestCase extends \PHPUnit_Framework_TestCase
protected $response;
protected $responses;
protected $catch;
+ protected $esi;
protected function setUp()
{
$this->kernel = null;
$this->cache = null;
+ $this->esi = null;
$this->caches = array();
$this->cacheConfig = array();
@@ -41,6 +45,7 @@ class HttpCacheTestCase extends \PHPUnit_Framework_TestCase
$this->response = null;
$this->responses = array();
+
$this->catch = false;
$this->clearDirectory(sys_get_temp_dir().'/http_cache');
@@ -101,7 +106,7 @@ class HttpCacheTestCase extends \PHPUnit_Framework_TestCase
$this->assertFalse($this->kernel->isCatchingExceptions());
}
- public function request($method, $uri = '/', $server = array(), $cookies = array())
+ public function request($method, $uri = '/', $server = array(), $cookies = array(), $esi = false)
{
if (null === $this->kernel) {
throw new \LogicException('You must call setNextResponse() before calling request().');
@@ -112,7 +117,9 @@ class HttpCacheTestCase extends \PHPUnit_Framework_TestCase
$this->store = new Store(sys_get_temp_dir().'/http_cache');
$this->cacheConfig['debug'] = true;
- $this->cache = new HttpCache($this->kernel, $this->store, null, $this->cacheConfig);
+
+ $this->esi = $esi ? new Esi() : null;
+ $this->cache = new HttpCache($this->kernel, $this->store, $this->esi, $this->cacheConfig);
$this->request = Request::create($uri, $method, array(), $cookies, array(), $server);
$this->response = $this->cache->handle($this->request, HttpKernelInterface::MASTER_REQUEST, $this->catch);
@@ -133,11 +140,14 @@ class HttpCacheTestCase extends \PHPUnit_Framework_TestCase
// A basic response with 200 status code and a tiny body.
public function setNextResponse($statusCode = 200, array $headers = array(), $body = 'Hello World', \Closure $customizer = null)
{
- $called = false;
-
$this->kernel = new TestHttpKernel($body, $statusCode, $headers, $customizer);
}
+ public function setNextResponses($responses)
+ {
+ $this->kernel = new TestMultipleHttpKernel($responses);
+ }
+
public function catchExceptions($catch = true)
{
$this->catch = $catch;
diff --git a/tests/Symfony/Tests/Component/HttpKernel/HttpCache/TestMultipleHttpKernel.php b/tests/Symfony/Tests/Component/HttpKernel/HttpCache/TestMultipleHttpKernel.php
new file mode 100644
index 0000000000..899e8c2b57
--- /dev/null
+++ b/tests/Symfony/Tests/Component/HttpKernel/HttpCache/TestMultipleHttpKernel.php
@@ -0,0 +1,78 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Tests\Component\HttpKernel\HttpCache;
+
+use Symfony\Component\HttpKernel\HttpKernel;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
+
+class TestMultipleHttpKernel extends HttpKernel implements ControllerResolverInterface
+{
+ protected $bodies;
+ protected $statuses;
+ protected $headers;
+ protected $catch;
+ protected $call;
+
+ public function __construct($responses)
+ {
+ $this->bodies = array();
+ $this->statuses = array();
+ $this->headers = array();
+ $this->call = false;
+
+ foreach ($responses as $response) {
+ $this->bodies[] = $response['body'];
+ $this->statuses[] = $response['status'];
+ $this->headers[] = $response['headers'];
+ }
+
+ parent::__construct(new EventDispatcher(), $this);
+ }
+
+ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = false)
+ {
+ return parent::handle($request, $type, $catch);
+ }
+
+ public function getController(Request $request)
+ {
+ return array($this, 'callController');
+ }
+
+ public function getArguments(Request $request, $controller)
+ {
+ return array($request);
+ }
+
+ public function callController(Request $request)
+ {
+ $this->called = true;
+
+ $response = new Response(array_shift($this->bodies), array_shift($this->statuses), array_shift($this->headers));
+
+ return $response;
+ }
+
+ public function hasBeenCalled()
+ {
+ return $this->called;
+ }
+
+ public function reset()
+ {
+ $this->call = false;
+ }
+}