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; + } +}