Browse Source

[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.
Marc Weistroff 14 years ago
parent
commit
bebdcb242d

+ 36 - 0
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.
      *

+ 63 - 0
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'    => '<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'));
+    }
 }

+ 14 - 4
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;

+ 78 - 0
tests/Symfony/Tests/Component/HttpKernel/HttpCache/TestMultipleHttpKernel.php

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