Skip to content

Commit 3aac420

Browse files
authored
Merge pull request #3 from mficzel/feature/seperateCacheHeaderAndCaching
FEATURE: CacheControl, ETag and If-None-Match header support
2 parents a766dea + 2a84f6c commit 3aac420

5 files changed

Lines changed: 203 additions & 74 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
namespace Flowpack\FullPageCache\Http;
3+
4+
use Neos\Flow\Annotations as Flow;
5+
use Neos\Flow\Http\Component\ComponentContext;
6+
use Neos\Flow\Http\Component\ComponentInterface;
7+
use Flowpack\FullPageCache\Cache\MetadataAwareStringFrontend;
8+
use Flowpack\FullPageCache\Aspects\ContentCacheAspect;
9+
10+
/**
11+
* Cache control header component
12+
*/
13+
class CacheControlHeaderComponent implements ComponentInterface
14+
{
15+
/**
16+
* @Flow\Inject
17+
* @var MetadataAwareStringFrontend
18+
*/
19+
protected $contentCache;
20+
21+
22+
/**
23+
* @var boolean
24+
* @Flow\InjectConfiguration(path="enabled")
25+
*/
26+
protected $enabled;
27+
28+
/**
29+
* @Flow\Inject
30+
* @var ContentCacheAspect
31+
*/
32+
protected $contentCacheAspect;
33+
34+
/**
35+
* @inheritDoc
36+
*/
37+
public function handle(ComponentContext $componentContext)
38+
{
39+
if (!$this->enabled) {
40+
return;
41+
}
42+
43+
$request = $componentContext->getHttpRequest();
44+
if (strtoupper($request->getMethod()) !== 'GET') {
45+
return;
46+
}
47+
48+
if (!empty($request->getUri()->getQuery())) {
49+
return;
50+
}
51+
52+
$response = $componentContext->getHttpResponse();
53+
54+
if ($response->hasHeader('X-From-FullPageCache')) {
55+
return;
56+
}
57+
58+
if ($this->contentCacheAspect->hasUncachedSegments())
59+
{
60+
return;
61+
}
62+
63+
if ($response->hasHeader('Set-Cookie')) {
64+
return;
65+
}
66+
67+
[$tags, $lifetime] = $this->getCacheTagsAndLifetime();
68+
69+
if ($tags) {
70+
$modifiedResponse = $response
71+
->withHeader('X-CacheLifetime', $lifetime)
72+
->withHeader('X-CacheTags', $tags);
73+
74+
$componentContext->replaceHttpResponse($modifiedResponse);
75+
}
76+
}
77+
78+
/**
79+
* Get cache tags and lifetime from the cache metadata that was extracted by the special cache frontend for content cache
80+
*
81+
* @return array with first "tags" and then "lifetime"
82+
*/
83+
protected function getCacheTagsAndLifetime(): array
84+
{
85+
$lifetime = null;
86+
$tags = [];
87+
$entriesMetadata = $this->contentCache->getAllMetadata();
88+
foreach ($entriesMetadata as $identifier => $metadata) {
89+
$entryTags = isset($metadata['tags']) ? $metadata['tags'] : [];
90+
$entryLifetime = isset($metadata['lifetime']) ? $metadata['lifetime'] : null;
91+
if ($entryLifetime !== null) {
92+
if ($lifetime === null) {
93+
$lifetime = $entryLifetime;
94+
} else {
95+
$lifetime = min($lifetime, $entryLifetime);
96+
}
97+
}
98+
$tags = array_unique(array_merge($tags, $entryTags));
99+
}
100+
101+
return [$tags, $lifetime];
102+
}
103+
}

Classes/Http/RequestInterceptorComponent.php

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Neos\Flow\Http\Component\ComponentInterface;
99
use Neos\Flow\Security\SessionDataContainer;
1010
use Neos\Flow\Session\SessionManagerInterface;
11+
use GuzzleHttp\Psr7\Response;
1112
use function GuzzleHttp\Psr7\parse_response;
1213

1314
/**
@@ -27,6 +28,12 @@ class RequestInterceptorComponent implements ComponentInterface
2728
*/
2829
protected $enabled;
2930

31+
/**
32+
* @var boolean
33+
* @Flow\InjectConfiguration(path="maxPublicCacheTime")
34+
*/
35+
protected $maxPublicCacheTime;
36+
3037
/**
3138
* @Flow\Inject(lazy=false)
3239
* @var SessionManagerInterface
@@ -66,12 +73,54 @@ public function handle(ComponentContext $componentContext)
6673
$entry = $this->cacheFrontend->get($entryIdentifier);
6774
if ($entry) {
6875
if (class_exists('Neos\\Flow\\Http\\Response')) {
69-
$response = \Neos\Flow\Http\Response::createFromRaw($entry);
76+
$cachedResponse = \Neos\Flow\Http\Response::createFromRaw($entry);
7077
} else {
71-
$response = parse_response($entry);
78+
$cachedResponse = parse_response($entry);
79+
}
80+
81+
$etag = $cachedResponse->getHeaderLine('ETag');
82+
$lifetime = (int)$cachedResponse->getHeaderLine('X-Storage-Lifetime');
83+
$timestamp = (int)$cachedResponse->getHeaderLine('X-Storage-Timestamp');
84+
85+
// return 304 not modified when possible
86+
$ifNoneMatch = $request->getHeaderLine('If-None-Match');
87+
if ($ifNoneMatch && $ifNoneMatch === $etag ) {
88+
if (class_exists('Neos\\Flow\\Http\\Response')) {
89+
$notModifiedResponse = new \Neos\Flow\Http\Response();
90+
} else {
91+
$notModifiedResponse = new Response();
92+
}
93+
$notModifiedResponse = $notModifiedResponse
94+
->withStatus(304)
95+
->withHeader('X-From-FullPageCache', $entryIdentifier);
96+
97+
$componentContext->replaceHttpResponse($notModifiedResponse);
98+
$componentContext->setParameter(ComponentChain::class, 'cancel', true);
99+
return;
100+
}
101+
102+
$cachedResponse = $cachedResponse
103+
->withoutHeader('X-Storage-Lifetime')
104+
->withoutHeader('X-Storage-Timestamp')
105+
->withHeader('X-From-FullPageCache', $entryIdentifier);
106+
107+
if ($this->maxPublicCacheTime > 0) {
108+
if ($lifetime > 0) {
109+
$remainingCacheTime = $lifetime - (time() - $timestamp);
110+
if ($remainingCacheTime > $this->maxPublicCacheTime) {
111+
$remainingCacheTime = $this->maxPublicCacheTime;
112+
}
113+
if ($remainingCacheTime > 0) {
114+
$cachedResponse = $cachedResponse
115+
->withHeader('CacheControl', 'max-age=' . $remainingCacheTime);
116+
}
117+
} else {
118+
$cachedResponse = $cachedResponse
119+
->withHeader('CacheControl', 'max-age=' . $this->maxPublicCacheTime);
120+
}
72121
}
73-
$response = $response->withHeader('X-From-FullPageCache', $entryIdentifier);
74-
$componentContext->replaceHttpResponse($response);
122+
123+
$componentContext->replaceHttpResponse($cachedResponse);
75124
$componentContext->setParameter(ComponentChain::class, 'cancel', true);
76125
}
77126
}
Lines changed: 30 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
<?php
22
namespace Flowpack\FullPageCache\Http;
33

4-
use Flowpack\FullPageCache\Aspects\ContentCacheAspect;
5-
use Flowpack\FullPageCache\Cache\MetadataAwareStringFrontend;
64
use Neos\Cache\Frontend\StringFrontend;
75
use Neos\Flow\Annotations as Flow;
86
use Neos\Flow\Http\Component\ComponentContext;
@@ -20,95 +18,63 @@ class RequestStorageComponent implements ComponentInterface
2018
*/
2119
protected $cacheFrontend;
2220

23-
/**
24-
* @Flow\Inject
25-
* @var MetadataAwareStringFrontend
26-
*/
27-
protected $contentCache;
28-
2921
/**
3022
* @var boolean
3123
* @Flow\InjectConfiguration(path="enabled")
3224
*/
3325
protected $enabled;
3426

3527
/**
36-
* @Flow\Inject
37-
* @var ContentCacheAspect
28+
* @var boolean
29+
* @Flow\InjectConfiguration(path="maxPublicCacheTime")
3830
*/
39-
protected $contentCacheAspect;
31+
protected $maxPublicCacheTime;
4032

4133
/**
4234
* @inheritDoc
4335
*/
4436
public function handle(ComponentContext $componentContext)
4537
{
46-
if (!$this->enabled) {
47-
return;
48-
}
49-
5038
$request = $componentContext->getHttpRequest();
51-
if (strtoupper($request->getMethod()) !== 'GET') {
52-
return;
53-
}
54-
55-
if (!empty($request->getUri()->getQuery())) {
56-
return;
57-
}
58-
5939
$response = $componentContext->getHttpResponse();
6040

6141
if ($response->hasHeader('X-From-FullPageCache')) {
6242
return;
6343
}
6444

65-
if ($this->contentCacheAspect->hasUncachedSegments())
66-
{
67-
return;
68-
}
45+
if ($response->hasHeader('X-CacheLifetime')) {
46+
$lifetime = (int)$response->getHeaderLine('X-CacheLifetime');
47+
$cacheTags = $response->getHeader('X-CacheTags') ;
48+
$entryIdentifier = md5((string)$request->getUri());
6949

70-
if ($response->hasHeader('Set-Cookie')) {
71-
return;
72-
}
50+
$publicLifetime = 0;
51+
if ($this->maxPublicCacheTime > 0) {
52+
if ($lifetime > 0 && $lifetime < $this->maxPublicCacheTime) {
53+
$publicLifetime = $lifetime;
54+
} else {
55+
$publicLifetime = $this->maxPublicCacheTime;
56+
}
57+
}
7358

74-
$entryIdentifier = md5((string)$request->getUri());
59+
$modifiedResponse = $response
60+
->withoutHeader('X-CacheTags')
61+
->withoutHeader('X-CacheLifetime');
7562

76-
[$tags, $lifetime] = $this->getCacheTagsAndLifetime();
63+
if ($publicLifetime > 0) {
64+
$entryContentHash = md5(str($response));
65+
$modifiedResponse = $modifiedResponse
66+
->withAddedHeader('ETag', $entryContentHash)
67+
->withAddedHeader('CacheControl', 'max-age=' . $publicLifetime);
68+
}
7769

78-
if (empty($tags)) {
79-
// For now do not cache something without tags (maybe it was not a Neos page)
80-
return;
81-
}
70+
$modifiedResponseforStorage = $modifiedResponse
71+
->withHeader('X-Storage-Timestamp', time())
72+
->withHeader('X-Storage-Lifetime', $lifetime);
8273

83-
$modifiedResponse = $response->withHeader('X-Storage-Component', $entryIdentifier);
84-
$this->cacheFrontend->set($entryIdentifier, str($modifiedResponse), $tags, $lifetime);
85-
// TODO: because stream is copied ot the modifiedResponse we would get empty output on first request
86-
$response->getBody()->rewind();
87-
}
74+
$this->cacheFrontend->set($entryIdentifier, str($modifiedResponseforStorage), $cacheTags, $lifetime);
8875

89-
/**
90-
* Get cache tags and lifetime from the cache metadata that was extracted by the special cache frontend for content cache
91-
*
92-
* @return array with first "tags" and then "lifetime"
93-
*/
94-
protected function getCacheTagsAndLifetime(): array
95-
{
96-
$lifetime = null;
97-
$tags = [];
98-
$entriesMetadata = $this->contentCache->getAllMetadata();
99-
foreach ($entriesMetadata as $identifier => $metadata) {
100-
$entryTags = isset($metadata['tags']) ? $metadata['tags'] : [];
101-
$entryLifetime = isset($metadata['lifetime']) ? $metadata['lifetime'] : null;
102-
if ($entryLifetime !== null) {
103-
if ($lifetime === null) {
104-
$lifetime = $entryLifetime;
105-
} else {
106-
$lifetime = min($lifetime, $entryLifetime);
107-
}
108-
}
109-
$tags = array_unique(array_merge($tags, $entryTags));
76+
$modifiedResponse->getBody()->rewind();
77+
$componentContext->replaceHttpResponse($modifiedResponse);
11078
}
111-
112-
return [$tags, $lifetime];
11379
}
11480
}

Configuration/Objects.yaml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
Flowpack\FullPageCache\Http\RequestInterceptorComponent:
1+
Flowpack\FullPageCache\Http\CacheControlHeaderComponent:
22
properties:
3-
cacheFrontend:
3+
contentCache:
44
object:
55
factoryObjectName: Neos\Flow\Cache\CacheManager
66
factoryMethodName: getCache
77
arguments:
88
1:
9-
value: Flowpack_FullPageCache_Entries
9+
value: Neos_Fusion_Content
1010

11-
Flowpack\FullPageCache\Http\RequestStorageComponent:
11+
Flowpack\FullPageCache\Http\RequestInterceptorComponent:
1212
properties:
1313
cacheFrontend:
1414
object:
@@ -17,13 +17,16 @@ Flowpack\FullPageCache\Http\RequestStorageComponent:
1717
arguments:
1818
1:
1919
value: Flowpack_FullPageCache_Entries
20-
contentCache:
20+
21+
Flowpack\FullPageCache\Http\RequestStorageComponent:
22+
properties:
23+
cacheFrontend:
2124
object:
2225
factoryObjectName: Neos\Flow\Cache\CacheManager
2326
factoryMethodName: getCache
2427
arguments:
2528
1:
26-
value: Neos_Fusion_Content
29+
value: Flowpack_FullPageCache_Entries
2730

2831
Flowpack\FullPageCache\Aspects\ContentCacheAspect:
2932
properties:

Configuration/Settings.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
Flowpack:
22
FullPageCache:
3+
# enable full page caching
34
enabled: true
5+
6+
# the maximum public cache control header sent
7+
# set to 0 if you do not want to send public CacheControl headers
8+
maxPublicCacheTime: 86400
49
Neos:
510
Flow:
611
http:
@@ -10,6 +15,9 @@ Neos:
1015
requestInterceptor:
1116
position: 'start 999999'
1217
component: 'Flowpack\FullPageCache\Http\RequestInterceptorComponent'
18+
addCacheHeader:
19+
position: 'after setHeader'
20+
component: 'Flowpack\FullPageCache\Http\CacheControlHeaderComponent'
1321
'postprocess':
1422
chain:
1523
requestStorage:

0 commit comments

Comments
 (0)