5
5
use Http \Client \Common \Plugin ;
6
6
use Http \Message \StreamFactory ;
7
7
use Http \Promise \FulfilledPromise ;
8
+ use Psr \Cache \CacheItemInterface ;
8
9
use Psr \Cache \CacheItemPoolInterface ;
9
10
use Psr \Http \Message \RequestInterface ;
10
11
use Psr \Http \Message \ResponseInterface ;
@@ -57,7 +58,6 @@ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamF
57
58
public function handleRequest (RequestInterface $ request , callable $ next , callable $ first )
58
59
{
59
60
$ method = strtoupper ($ request ->getMethod ());
60
-
61
61
// if the request not is cachable, move to $next
62
62
if ($ method !== 'GET ' && $ method !== 'HEAD ' ) {
63
63
return $ next ($ request );
@@ -68,15 +68,47 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
68
68
$ cacheItem = $ this ->pool ->getItem ($ key );
69
69
70
70
if ($ cacheItem ->isHit ()) {
71
- // return cached response
72
71
$ data = $ cacheItem ->get ();
73
- $ response = $ data ['response ' ];
74
- $ response = $ response ->withBody ($ this ->streamFactory ->createStream ($ data ['body ' ]));
72
+ if (isset ($ data ['expiresAt ' ]) && time () > $ data ['expiresAt ' ]) {
73
+ // This item is still valid according to previous cache headers
74
+ return new FulfilledPromise ($ this ->createResponseFromCacheItem ($ cacheItem ));
75
+ }
76
+
77
+ // Add headers to ask the server if this cache is still valid
78
+ if ($ modifiedAt = $ this ->getModifiedAt ($ cacheItem )) {
79
+ $ modifiedAt = new \DateTime ('@ ' .$ modifiedAt );
80
+ $ modifiedAt ->setTimezone (new \DateTimeZone ('GMT ' ));
81
+ $ request = $ request ->withHeader (
82
+ 'If-Modified-Since ' ,
83
+ sprintf ('%s GMT ' , $ modifiedAt ->format ('l, d-M-y H:i:s ' ))
84
+ );
85
+ }
75
86
76
- return new FulfilledPromise ($ response );
87
+ if ($ etag = $ this ->getETag ($ cacheItem )) {
88
+ $ request = $ request ->withHeader (
89
+ 'If-None-Match ' ,
90
+ $ etag
91
+ );
92
+ }
77
93
}
78
94
95
+
79
96
return $ next ($ request )->then (function (ResponseInterface $ response ) use ($ cacheItem ) {
97
+ if (304 === $ response ->getStatusCode ()) {
98
+ if (!$ cacheItem ->isHit ()) {
99
+ // We do not have the item in cache. We can return the cached response.
100
+ return $ response ;
101
+ }
102
+
103
+ // The cached response we have is still valid
104
+ $ data = $ cacheItem ->get ();
105
+ $ data ['expiresAt ' ] = time () + $ this ->getMaxAge ($ response );
106
+ $ cacheItem ->set ($ data )->expiresAfter ($ this ->config ['cache_lifetime ' ] + $ data ['expiresAt ' ]);
107
+ $ this ->pool ->save ($ cacheItem );
108
+
109
+ return $ this ->createResponseFromCacheItem ($ cacheItem );
110
+ }
111
+
80
112
if ($ this ->isCacheable ($ response )) {
81
113
$ bodyStream = $ response ->getBody ();
82
114
$ body = $ bodyStream ->__toString ();
@@ -86,8 +118,16 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
86
118
$ response = $ response ->withBody ($ this ->streamFactory ->createStream ($ body ));
87
119
}
88
120
89
- $ cacheItem ->set (['response ' => $ response , 'body ' => $ body ])
90
- ->expiresAfter ($ this ->getMaxAge ($ response ));
121
+ $ expiresAt = time () + $ this ->getMaxAge ($ response );
122
+ $ cacheItem
123
+ ->expiresAfter ($ this ->config ['cache_lifetime ' ] + $ expiresAt )
124
+ ->set ([
125
+ 'response ' => $ response ,
126
+ 'body ' => $ body ,
127
+ 'expiresAt ' => $ expiresAt ,
128
+ 'createdAt ' => time (),
129
+ 'etag ' => $ response ->getHeader ('ETag ' ),
130
+ ]);
91
131
$ this ->pool ->save ($ cacheItem );
92
132
}
93
133
@@ -194,11 +234,73 @@ private function getMaxAge(ResponseInterface $response)
194
234
private function configureOptions (OptionsResolver $ resolver )
195
235
{
196
236
$ resolver ->setDefaults ([
237
+ 'cache_lifetime ' => 2592000 , // 30 days
197
238
'default_ttl ' => null ,
198
239
'respect_cache_headers ' => true ,
199
240
]);
200
241
242
+ $ resolver ->setAllowedTypes ('cache_lifetime ' , 'int ' );
201
243
$ resolver ->setAllowedTypes ('default_ttl ' , ['int ' , 'null ' ]);
202
244
$ resolver ->setAllowedTypes ('respect_cache_headers ' , 'bool ' );
203
245
}
246
+
247
+ /**
248
+ * @param CacheItemInterface $cacheItem
249
+ *
250
+ * @return ResponseInterface
251
+ */
252
+ private function createResponseFromCacheItem (CacheItemInterface $ cacheItem )
253
+ {
254
+ $ data = $ cacheItem ->get ();
255
+
256
+ /** @var ResponseInterface $response */
257
+ $ response = $ data ['response ' ];
258
+ $ response = $ response ->withBody ($ this ->streamFactory ->createStream ($ data ['body ' ]));
259
+
260
+ return $ response ;
261
+ }
262
+
263
+ /**
264
+ * Get the timestamp when the cached response was stored.
265
+ *
266
+ * @param CacheItemInterface $cacheItem
267
+ *
268
+ * @return int|null
269
+ */
270
+ private function getModifiedAt (CacheItemInterface $ cacheItem )
271
+ {
272
+ $ data = $ cacheItem ->get ();
273
+ if (!isset ($ data ['createdAt ' ])) {
274
+ return ;
275
+ }
276
+
277
+ return $ data ['createdAt ' ];
278
+ }
279
+
280
+ /**
281
+ * Get the ETag from the cached response.
282
+ *
283
+ * @param CacheItemInterface $cacheItem
284
+ *
285
+ * @return string|null
286
+ */
287
+ private function getETag (CacheItemInterface $ cacheItem )
288
+ {
289
+ $ data = $ cacheItem ->get ();
290
+ if (!isset ($ data ['etag ' ])) {
291
+ return ;
292
+ }
293
+
294
+ if (is_array ($ data ['etag ' ])) {
295
+ foreach ($ data ['etag ' ] as $ etag ) {
296
+ if (!empty ($ etag )) {
297
+ return $ etag ;
298
+ }
299
+ }
300
+
301
+ return ;
302
+ }
303
+
304
+ return $ data ['etag ' ];
305
+ }
204
306
}
0 commit comments