[HttpKernel] Added response cache-control modification if page is composed of ESIs.

Rules are :
- If one of the ESI has validation cache strategy, the whole page will be
forced to validate.
- In none of the ESI has validation, the response will feature a Cache-Control
directive with s-maxage value equals to the smallest TTL of ESIs.
This commit is contained in:
Marc Weistroff 2011-02-06 16:06:54 -08:00 committed by Fabien Potencier
parent 2201382fa1
commit bebdcb242d
4 changed files with 191 additions and 4 deletions

View File

@ -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.
*

View File

@ -893,4 +893,67 @@ class HttpCacheTest extends HttpCacheTestCase
$this->assertExceptionsAreNotCaught();
}
public function testEsiCacheSendsTheLowestTtl()
{
$responses = array(
array(
'status' => 200,
'body' => '<esi:include src="/foo" /> <esi:include src="/bar" />',
'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' => '<esi:include src="/foo" /> <esi:include src="/bar" />',
'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'));
}
}

View File

@ -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;

View File

@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* 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;
}
}