diff --git a/src/Cache.php b/src/Cache.php index 769d8ee..a1e67a1 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -11,6 +11,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -45,18 +46,29 @@ class Cache implements MiddlewareInterface */ protected $mustRevalidate; + /** + * @var StreamFactoryInterface|null + */ + protected $streamFactory; + /** * Create new HTTP cache * - * @param string $type The cache type: "public" or "private" - * @param int $maxAge The maximum age of client-side cache - * @param bool $mustRevalidate must-revalidate + * @param string $type The cache type: "public" or "private" + * @param int $maxAge The maximum age of client-side cache + * @param bool $mustRevalidate must-revalidate + * @param StreamFactoryInterface|null $streamFactory Will clear body of a 304 if provided */ - public function __construct(string $type = 'private', int $maxAge = 86400, bool $mustRevalidate = false) - { + public function __construct( + string $type = 'private', + int $maxAge = 86400, + bool $mustRevalidate = false, + ?StreamFactoryInterface $streamFactory = null + ) { $this->type = $type; $this->maxAge = $maxAge; $this->mustRevalidate = $mustRevalidate; + $this->streamFactory = $streamFactory; } /** @@ -101,7 +113,11 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface if ($ifNoneMatch) { $etagList = preg_split('@\s*,\s*@', $ifNoneMatch); if (is_array($etagList) && (in_array($etag, $etagList) || in_array('*', $etagList))) { - return $response->withStatus(304); + $response = $response->withStatus(304); + if ($this->streamFactory) { + $response = $response->withBody($this->streamFactory->createStream('')); + } + return $response; } } } @@ -118,7 +134,11 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $ifModifiedSince = $request->getHeaderLine('If-Modified-Since'); if ($ifModifiedSince && $lastModified <= strtotime($ifModifiedSince)) { - return $response->withStatus(304); + $response = $response->withStatus(304); + if ($this->streamFactory) { + $response = $response->withBody($this->streamFactory->createStream('')); + } + return $response; } } diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 47b595c..df8a39b 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -15,6 +15,7 @@ use Slim\HttpCache\Cache; use Slim\Psr7\Factory\ResponseFactory; use Slim\Psr7\Factory\ServerRequestFactory; +use Slim\Psr7\Factory\StreamFactory; use function gmdate; use function time; @@ -64,7 +65,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface public function testCacheControlHeader() { - $cache = new Cache('public', 86400); + $cache = $this->createCache('public', 86400, false, false); $req = $this->requestFactory(); $res = $cache->process($req, $this->createRequestHandler(null)); @@ -76,7 +77,7 @@ public function testCacheControlHeader() public function testCacheControlHeaderWithMustRevalidate() { - $cache = new Cache('private', 86400, true); + $cache = $this->createCache('private', 86400, true, false); $req = $this->requestFactory(); $res = $cache->process($req, $this->createRequestHandler(null)); @@ -88,7 +89,7 @@ public function testCacheControlHeaderWithMustRevalidate() public function testCacheControlHeaderWithZeroMaxAge() { - $cache = new Cache('private', 0, false); + $cache = $this->createCache('private', 0, false, false); $req = $this->requestFactory(); $res = $cache->process($req, $this->createRequestHandler(null)); @@ -100,7 +101,7 @@ public function testCacheControlHeaderWithZeroMaxAge() public function testCacheControlHeaderDoesNotOverrideExistingHeader() { - $cache = new Cache('public', 86400); + $cache = $this->createCache('public', 86400, false, false); $req = $this->requestFactory(); $res = $this->createResponse()->withHeader('Cache-Control', 'no-cache,no-store'); @@ -116,14 +117,16 @@ public function testLastModifiedWithCacheHit() $now = time(); $lastModified = gmdate('D, d M Y H:i:s T', $now + 86400); $ifModifiedSince = gmdate('D, d M Y H:i:s T', $now + 86400); - $cache = new Cache('public', 86400); + $cache = $this->createCache('public', 86400, false, false); $req = $this->requestFactory()->withHeader('If-Modified-Since', $ifModifiedSince); $res = $this->createResponse()->withHeader('Last-Modified', $lastModified); + $res->getBody()->write('payload data'); $res = $cache->process($req, $this->createRequestHandler($res)); $this->assertEquals(304, $res->getStatusCode()); + self::assertSame('payload data', (string) $res->getBody()); } public function testLastModifiedWithCacheHitAndNewerDate() @@ -131,7 +134,7 @@ public function testLastModifiedWithCacheHitAndNewerDate() $now = time(); $lastModified = gmdate('D, d M Y H:i:s T', $now + 86400); $ifModifiedSince = gmdate('D, d M Y H:i:s T', $now + 172800); // <-- Newer date - $cache = new Cache('public', 86400); + $cache = $this->createCache('public', 86400, false, false); $req = $this->requestFactory()->withHeader('If-Modified-Since', $ifModifiedSince); $res = $this->createResponse()->withHeader('Last-Modified', $lastModified); @@ -145,7 +148,7 @@ public function testLastModifiedWithCacheHitAndOlderDate() $now = time(); $lastModified = gmdate('D, d M Y H:i:s T', $now + 86400); $ifModifiedSince = gmdate('D, d M Y H:i:s T', $now); // <-- Older date - $cache = new Cache('public', 86400); + $cache = $this->createCache('public', 86400, false, false); $req = $this->requestFactory()->withHeader('If-Modified-Since', $ifModifiedSince); $res = $this->createResponse()->withHeader('Last-Modified', $lastModified); @@ -159,7 +162,7 @@ public function testLastModifiedWithCacheMiss() $now = time(); $lastModified = gmdate('D, d M Y H:i:s T', $now + 86400); $ifModifiedSince = gmdate('D, d M Y H:i:s T', $now - 86400); - $cache = new Cache('public', 86400); + $cache = $this->createCache('public', 86400, false, false); $req = $this->requestFactory()->withHeader('If-Modified-Since', $ifModifiedSince); $res = $this->createResponse()->withHeader('Last-Modified', $lastModified); @@ -172,20 +175,22 @@ public function testETagWithCacheHit() { $etag = 'abc'; $ifNoneMatch = 'abc'; - $cache = new Cache('public', 86400); + $cache = $this->createCache('public', 86400, false, false); $req = $this->requestFactory()->withHeader('If-None-Match', $ifNoneMatch); $res = $this->createResponse()->withHeader('Etag', $etag); + $res->getBody()->write('payload data'); $res = $cache->process($req, $this->createRequestHandler($res)); $this->assertEquals(304, $res->getStatusCode()); + self::assertSame('payload data', (string) $res->getBody()); } public function testETagWithCacheMiss() { $etag = 'abc'; $ifNoneMatch = 'xyz'; - $cache = new Cache('public', 86400); + $cache = $this->createCache('public', 86400, false, false); $req = $this->requestFactory()->withHeader('If-None-Match', $ifNoneMatch); $res = $this->createResponse()->withHeader('Etag', $etag); @@ -193,4 +198,41 @@ public function testETagWithCacheMiss() $this->assertEquals(200, $res->getStatusCode()); } + + public function testETagReturnsNoBodyOnCacheHitWhenAStreamFactoryIsProvided(): void + { + $etag = 'abc'; + $cache = $this->createCache('private', 86400, false, true); + $req = $this->requestFactory()->withHeader('If-None-Match', $etag); + + $res = $this->createResponse()->withHeader('Etag', $etag); + $res->getBody()->write('payload data'); + $res = $cache->process($req, $this->createRequestHandler($res)); + + self::assertSame(304, $res->getStatusCode()); + self::assertSame('', (string) $res->getBody()); + } + + public function testLastModifiedReturnsNoBodyOnCacheHitWhenAStreamFactoryIsProvided(): void + { + $now = time() + 86400; + $lastModified = gmdate('D, d M Y H:i:s T', $now); + $ifModifiedSince = gmdate('D, d M Y H:i:s T', $now); + $cache = $this->createCache('private', 86400, false, true); + + $req = $this->requestFactory()->withHeader('If-Modified-Since', $ifModifiedSince); + $res = $this->createResponse()->withHeader('Last-Modified', $lastModified); + $res->getBody()->write('payload data'); + + $res = $cache->process($req, $this->createRequestHandler($res)); + + self::assertEquals(304, $res->getStatusCode()); + self::assertSame('', (string) $res->getBody()); + } + + private function createCache(string $type, int $maxAge, bool $mustRevalidate, bool $withStreamFactory): Cache + { + $streamFactory = $withStreamFactory ? new StreamFactory() : null; + return new Cache($type, $maxAge, $mustRevalidate, $streamFactory); + } }