1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Kevinrob\GuzzleCache; |
4
|
|
|
|
5
|
|
|
use GuzzleHttp\Client; |
6
|
|
|
use GuzzleHttp\Exception\TransferException; |
7
|
|
|
use GuzzleHttp\Promise\FulfilledPromise; |
8
|
|
|
use GuzzleHttp\Promise\Promise; |
9
|
|
|
use GuzzleHttp\Promise\RejectedPromise; |
10
|
|
|
use GuzzleHttp\Psr7\Response; |
11
|
|
|
use Kevinrob\GuzzleCache\Strategy\CacheStrategyInterface; |
12
|
|
|
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy; |
13
|
|
|
use Psr\Http\Message\RequestInterface; |
14
|
|
|
use Psr\Http\Message\ResponseInterface; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* Class CacheMiddleware. |
18
|
|
|
*/ |
19
|
|
|
class CacheMiddleware |
20
|
|
|
{ |
21
|
|
|
const HEADER_RE_VALIDATION = 'X-Kevinrob-GuzzleCache-ReValidation'; |
22
|
|
|
const HEADER_INVALIDATION = 'X-Kevinrob-GuzzleCache-Invalidation'; |
23
|
|
|
const HEADER_CACHE_INFO = 'X-Kevinrob-Cache'; |
24
|
|
|
const HEADER_CACHE_HIT = 'HIT'; |
25
|
|
|
const HEADER_CACHE_MISS = 'MISS'; |
26
|
|
|
const HEADER_CACHE_STALE = 'STALE'; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @var array of Promise |
30
|
|
|
*/ |
31
|
|
|
protected $waitingRevalidate = []; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @var Client |
35
|
|
|
*/ |
36
|
|
|
protected $client; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @var CacheStrategyInterface |
40
|
|
|
*/ |
41
|
|
|
protected $cacheStorage; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* List of allowed HTTP methods to cache |
45
|
|
|
* Key = method name (upscaling) |
46
|
|
|
* Value = true. |
47
|
|
|
* |
48
|
|
|
* @var array |
49
|
|
|
*/ |
50
|
|
|
protected $httpMethods = ['GET' => true]; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* @param CacheStrategyInterface|null $cacheStrategy |
54
|
|
|
*/ |
55
|
44 |
|
public function __construct(CacheStrategyInterface $cacheStrategy = null) |
56
|
1 |
|
{ |
57
|
44 |
|
$this->cacheStorage = $cacheStrategy !== null ? $cacheStrategy : new PrivateCacheStrategy(); |
58
|
|
|
|
59
|
44 |
|
register_shutdown_function([$this, 'purgeReValidation']); |
60
|
44 |
|
} |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* @param Client $client |
64
|
|
|
*/ |
65
|
3 |
|
public function setClient(Client $client) |
66
|
|
|
{ |
67
|
3 |
|
$this->client = $client; |
68
|
3 |
|
} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* @param CacheStrategyInterface $cacheStorage |
72
|
|
|
*/ |
73
|
|
|
public function setCacheStorage(CacheStrategyInterface $cacheStorage) |
74
|
|
|
{ |
75
|
|
|
$this->cacheStorage = $cacheStorage; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* @return CacheStrategyInterface |
80
|
|
|
*/ |
81
|
|
|
public function getCacheStorage() |
82
|
|
|
{ |
83
|
|
|
return $this->cacheStorage; |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* @param array $methods |
88
|
|
|
*/ |
89
|
|
|
public function setHttpMethods(array $methods) |
90
|
|
|
{ |
91
|
|
|
$this->httpMethods = $methods; |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
public function getHttpMethods() |
95
|
|
|
{ |
96
|
|
|
return $this->httpMethods; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* Will be called at the end of the script. |
101
|
|
|
*/ |
102
|
1 |
|
public function purgeReValidation() |
103
|
|
|
{ |
104
|
1 |
|
\GuzzleHttp\Promise\inspect_all($this->waitingRevalidate); |
105
|
1 |
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* @param callable $handler |
109
|
|
|
* |
110
|
|
|
* @return callable |
111
|
|
|
*/ |
112
|
43 |
|
public function __invoke(callable $handler) |
113
|
|
|
{ |
114
|
|
|
return function (RequestInterface $request, array $options) use (&$handler) { |
115
|
43 |
|
if (!isset($this->httpMethods[strtoupper($request->getMethod())])) { |
116
|
|
|
// No caching for this method allowed |
117
|
|
|
|
118
|
3 |
|
return $handler($request, $options)->then( |
119
|
|
|
function (ResponseInterface $response) use ($request) { |
120
|
|
|
// Invalidate cache after a call of non-safe method on the same URI |
121
|
3 |
|
$response = $this->invalidateCache($request, $response); |
122
|
|
|
|
123
|
3 |
|
return $response->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_MISS); |
124
|
|
|
} |
125
|
3 |
|
); |
126
|
|
|
} |
127
|
|
|
|
128
|
41 |
|
if ($request->hasHeader(self::HEADER_RE_VALIDATION)) { |
129
|
|
|
// It's a re-validation request, so bypass the cache! |
130
|
1 |
|
return $handler($request->withoutHeader(self::HEADER_RE_VALIDATION), $options); |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
// Retrieve information from request (Cache-Control) |
134
|
41 |
|
$reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control')); |
135
|
41 |
|
$onlyFromCache = $reqCacheControl->has('only-if-cached'); |
136
|
41 |
|
$staleResponse = $reqCacheControl->has('max-stale') |
137
|
41 |
|
&& $reqCacheControl->get('max-stale') === ''; |
138
|
41 |
|
$maxStaleCache = $reqCacheControl->get('max-stale', null); |
139
|
41 |
|
$minFreshCache = $reqCacheControl->get('min-fresh', null); |
140
|
|
|
|
141
|
|
|
// If cache => return new FulfilledPromise(...) with response |
142
|
41 |
|
$cacheEntry = $this->cacheStorage->fetch($request); |
143
|
41 |
|
if ($cacheEntry instanceof CacheEntry) { |
144
|
34 |
|
$body = $cacheEntry->getResponse()->getBody(); |
145
|
34 |
|
if ($body->tell() > 0) { |
146
|
2 |
|
$body->rewind(); |
147
|
2 |
|
} |
148
|
|
|
|
149
|
34 |
|
if ($cacheEntry->isFresh() |
150
|
34 |
|
&& ($minFreshCache === null || $cacheEntry->getStaleAge() + (int)$minFreshCache <= 0) |
151
|
34 |
|
) { |
152
|
|
|
// Cache HIT! |
153
|
28 |
|
return new FulfilledPromise( |
154
|
28 |
|
$cacheEntry->getResponse()->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_HIT) |
155
|
28 |
|
); |
156
|
|
|
} elseif ($staleResponse |
157
|
15 |
|
|| ($maxStaleCache !== null && $cacheEntry->getStaleAge() <= $maxStaleCache) |
158
|
15 |
|
) { |
159
|
|
|
// Staled cache! |
160
|
|
|
return new FulfilledPromise( |
161
|
|
|
$cacheEntry->getResponse()->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_HIT) |
162
|
|
|
); |
163
|
15 |
|
} elseif ($cacheEntry->hasValidationInformation() && !$onlyFromCache) { |
164
|
|
|
// Re-validation header |
165
|
4 |
|
$request = static::getRequestWithReValidationHeader($request, $cacheEntry); |
166
|
|
|
|
167
|
4 |
|
if ($cacheEntry->staleWhileValidate()) { |
168
|
1 |
|
static::addReValidationRequest($request, $this->cacheStorage, $cacheEntry); |
169
|
|
|
|
170
|
1 |
|
return new FulfilledPromise( |
171
|
1 |
|
$cacheEntry->getResponse() |
172
|
1 |
|
->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_STALE) |
173
|
1 |
|
); |
174
|
|
|
} |
175
|
3 |
|
} |
176
|
14 |
|
} else { |
177
|
41 |
|
$cacheEntry = null; |
178
|
|
|
} |
179
|
|
|
|
180
|
41 |
|
if ($cacheEntry === null && $onlyFromCache) { |
181
|
|
|
// Explicit asking of a cached response => 504 |
182
|
1 |
|
return new FulfilledPromise( |
183
|
1 |
|
new Response(504) |
184
|
1 |
|
); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** @var Promise $promise */ |
188
|
41 |
|
$promise = $handler($request, $options); |
189
|
|
|
|
190
|
41 |
|
return $promise->then( |
191
|
|
|
function (ResponseInterface $response) use ($request, $cacheEntry) { |
192
|
|
|
// Check if error and looking for a staled content |
193
|
41 |
|
if ($response->getStatusCode() >= 500) { |
194
|
1 |
|
$responseStale = static::getStaleResponse($cacheEntry); |
195
|
1 |
|
if ($responseStale instanceof ResponseInterface) { |
196
|
1 |
|
return $responseStale; |
197
|
|
|
} |
198
|
|
|
} |
199
|
|
|
|
200
|
41 |
|
$update = false; |
201
|
|
|
|
202
|
41 |
|
if ($response->getStatusCode() == 304 && $cacheEntry instanceof CacheEntry) { |
203
|
|
|
// Not modified => cache entry is re-validate |
204
|
|
|
/** @var ResponseInterface $response */ |
205
|
|
|
$response = $response |
206
|
2 |
|
->withStatus($cacheEntry->getResponse()->getStatusCode()) |
207
|
2 |
|
->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_HIT); |
208
|
2 |
|
$response = $response->withBody($cacheEntry->getResponse()->getBody()); |
209
|
|
|
|
210
|
|
|
// Merge headers of the "304 Not Modified" and the cache entry |
211
|
|
|
/** |
212
|
|
|
* @var string $headerName |
213
|
|
|
* @var string[] $headerValue |
214
|
|
|
*/ |
215
|
2 |
View Code Duplication |
foreach ($cacheEntry->getOriginalResponse()->getHeaders() as $headerName => $headerValue) { |
|
|
|
|
216
|
2 |
|
if (!$response->hasHeader($headerName) && $headerName !== self::HEADER_CACHE_INFO) { |
217
|
2 |
|
$response = $response->withHeader($headerName, $headerValue); |
218
|
2 |
|
} |
219
|
2 |
|
} |
220
|
|
|
|
221
|
2 |
|
$update = true; |
222
|
2 |
|
} else { |
223
|
41 |
|
$response = $response->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_MISS); |
224
|
|
|
} |
225
|
|
|
|
226
|
41 |
|
return static::addToCache($this->cacheStorage, $request, $response, $update); |
227
|
41 |
|
}, |
228
|
|
|
function ($reason) use ($cacheEntry) { |
229
|
|
|
if ($reason instanceof TransferException) { |
230
|
|
|
$response = static::getStaleResponse($cacheEntry); |
231
|
|
|
if ($response instanceof ResponseInterface) { |
232
|
|
|
return $response; |
233
|
|
|
} |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
return new RejectedPromise($reason); |
237
|
|
|
} |
238
|
41 |
|
); |
239
|
43 |
|
}; |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* @param CacheStrategyInterface $cache |
244
|
|
|
* @param RequestInterface $request |
245
|
|
|
* @param ResponseInterface $response |
246
|
|
|
* @param bool $update cache |
247
|
|
|
* @return ResponseInterface |
248
|
|
|
*/ |
249
|
41 |
|
protected static function addToCache( |
250
|
|
|
CacheStrategyInterface $cache, |
251
|
|
|
RequestInterface $request, |
252
|
|
|
ResponseInterface $response, |
253
|
|
|
$update = false |
254
|
|
|
) { |
255
|
|
|
// If the body is not seekable, we have to replace it by a seekable one |
256
|
41 |
|
if (!$response->getBody()->isSeekable()) { |
257
|
1 |
|
$response = $response->withBody( |
258
|
1 |
|
\GuzzleHttp\Psr7\stream_for($response->getBody()->getContents()) |
259
|
1 |
|
); |
260
|
1 |
|
} |
261
|
|
|
|
262
|
41 |
|
if ($update) { |
263
|
3 |
|
$cache->update($request, $response); |
264
|
3 |
|
} else { |
265
|
41 |
|
$cache->cache($request, $response); |
266
|
|
|
} |
267
|
|
|
|
268
|
41 |
|
return $response; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* @param RequestInterface $request |
273
|
|
|
* @param CacheStrategyInterface $cacheStorage |
274
|
|
|
* @param CacheEntry $cacheEntry |
275
|
|
|
* |
276
|
|
|
* @return bool if added |
277
|
|
|
*/ |
278
|
1 |
|
protected function addReValidationRequest( |
279
|
|
|
RequestInterface $request, |
280
|
|
|
CacheStrategyInterface &$cacheStorage, |
281
|
|
|
CacheEntry $cacheEntry |
282
|
|
|
) { |
283
|
|
|
// Add the promise for revalidate |
284
|
1 |
|
if ($this->client !== null) { |
285
|
|
|
/** @var RequestInterface $request */ |
286
|
1 |
|
$request = $request->withHeader(self::HEADER_RE_VALIDATION, '1'); |
287
|
1 |
|
$this->waitingRevalidate[] = $this->client |
288
|
1 |
|
->sendAsync($request) |
289
|
1 |
|
->then(function (ResponseInterface $response) use ($request, &$cacheStorage, $cacheEntry) { |
290
|
1 |
|
$update = false; |
291
|
|
|
|
292
|
1 |
|
if ($response->getStatusCode() == 304) { |
293
|
|
|
// Not modified => cache entry is re-validate |
294
|
|
|
/** @var ResponseInterface $response */ |
295
|
1 |
|
$response = $response->withStatus($cacheEntry->getResponse()->getStatusCode()); |
296
|
1 |
|
$response = $response->withBody($cacheEntry->getResponse()->getBody()); |
297
|
|
|
|
298
|
|
|
// Merge headers of the "304 Not Modified" and the cache entry |
299
|
1 |
View Code Duplication |
foreach ($cacheEntry->getResponse()->getHeaders() as $headerName => $headerValue) { |
|
|
|
|
300
|
1 |
|
if (!$response->hasHeader($headerName)) { |
301
|
1 |
|
$response = $response->withHeader($headerName, $headerValue); |
302
|
1 |
|
} |
303
|
1 |
|
} |
304
|
|
|
|
305
|
1 |
|
$update = true; |
306
|
1 |
|
} |
307
|
|
|
|
308
|
1 |
|
static::addToCache($cacheStorage, $request, $response, $update); |
309
|
1 |
|
}); |
310
|
|
|
|
311
|
1 |
|
return true; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
return false; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* @param CacheEntry|null $cacheEntry |
319
|
|
|
* |
320
|
|
|
* @return null|ResponseInterface |
321
|
|
|
*/ |
322
|
1 |
|
protected static function getStaleResponse(CacheEntry $cacheEntry = null) |
323
|
|
|
{ |
324
|
|
|
// Return staled cache entry if we can |
325
|
1 |
|
if ($cacheEntry instanceof CacheEntry && $cacheEntry->serveStaleIfError()) { |
326
|
1 |
|
return $cacheEntry->getResponse() |
327
|
1 |
|
->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_STALE); |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
return; |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* @param RequestInterface $request |
335
|
|
|
* @param CacheEntry $cacheEntry |
336
|
|
|
* |
337
|
|
|
* @return RequestInterface |
338
|
|
|
*/ |
339
|
4 |
|
protected static function getRequestWithReValidationHeader(RequestInterface $request, CacheEntry $cacheEntry) |
340
|
|
|
{ |
341
|
4 |
|
if ($cacheEntry->getResponse()->hasHeader('Last-Modified')) { |
342
|
|
|
$request = $request->withHeader( |
343
|
|
|
'If-Modified-Since', |
344
|
|
|
$cacheEntry->getResponse()->getHeader('Last-Modified') |
345
|
|
|
); |
346
|
|
|
} |
347
|
4 |
|
if ($cacheEntry->getResponse()->hasHeader('Etag')) { |
348
|
4 |
|
$request = $request->withHeader( |
349
|
4 |
|
'If-None-Match', |
350
|
4 |
|
$cacheEntry->getResponse()->getHeader('Etag') |
351
|
4 |
|
); |
352
|
4 |
|
} |
353
|
|
|
|
354
|
4 |
|
return $request; |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
/** |
358
|
|
|
* @param CacheStrategyInterface|null $cacheStorage |
359
|
|
|
* |
360
|
|
|
* @return CacheMiddleware the Middleware for Guzzle HandlerStack |
361
|
|
|
* |
362
|
|
|
* @deprecated Use constructor => `new CacheMiddleware()` |
363
|
|
|
*/ |
364
|
|
|
public static function getMiddleware(CacheStrategyInterface $cacheStorage = null) |
365
|
|
|
{ |
366
|
|
|
return new self($cacheStorage); |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* @param RequestInterface $request |
371
|
|
|
* |
372
|
|
|
* @param ResponseInterface $response |
373
|
|
|
* |
374
|
|
|
* @return ResponseInterface |
375
|
|
|
*/ |
376
|
3 |
|
private function invalidateCache(RequestInterface $request, ResponseInterface $response) |
377
|
|
|
{ |
378
|
3 |
|
$this->cacheStorage->delete($request); |
379
|
|
|
|
380
|
3 |
|
return $response->withHeader(self::HEADER_INVALIDATION, true); |
|
|
|
|
381
|
|
|
} |
382
|
|
|
} |
383
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.