Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
     * List of safe methods
54
     *
55
     * https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
56
     *
57
     * @var array
58
     */
59
    protected $safeMethods = ['GET' => true, 'HEAD' => true, 'OPTIONS' => true, 'TRACE' => true];
60
 
61
    /**
62
     * @param CacheStrategyInterface|null $cacheStrategy
63
     */
1441 ariadna 64
    public function __construct(?CacheStrategyInterface $cacheStrategy = null)
1 efrain 65
    {
66
        $this->cacheStorage = $cacheStrategy !== null ? $cacheStrategy : new PrivateCacheStrategy();
67
 
68
        register_shutdown_function([$this, 'purgeReValidation']);
69
    }
70
 
71
    /**
72
     * @param Client $client
73
     */
74
    public function setClient(Client $client)
75
    {
76
        $this->client = $client;
77
    }
78
 
79
    /**
80
     * @param CacheStrategyInterface $cacheStorage
81
     */
82
    public function setCacheStorage(CacheStrategyInterface $cacheStorage)
83
    {
84
        $this->cacheStorage = $cacheStorage;
85
    }
86
 
87
    /**
88
     * @return CacheStrategyInterface
89
     */
90
    public function getCacheStorage()
91
    {
92
        return $this->cacheStorage;
93
    }
94
 
95
    /**
96
     * @param array $methods
97
     */
98
    public function setHttpMethods(array $methods)
99
    {
100
        $this->httpMethods = $methods;
101
    }
102
 
103
    public function getHttpMethods()
104
    {
105
        return $this->httpMethods;
106
    }
107
 
108
    /**
109
     * Will be called at the end of the script.
110
     */
111
    public function purgeReValidation()
112
    {
1441 ariadna 113
        \GuzzleHttp\Promise\Utils::inspectAll($this->waitingRevalidate);
1 efrain 114
    }
115
 
116
    /**
117
     * @param callable $handler
118
     *
119
     * @return callable
120
     */
121
    public function __invoke(callable $handler)
122
    {
123
        return function (RequestInterface $request, array $options) use (&$handler) {
124
            if (!isset($this->httpMethods[strtoupper($request->getMethod())])) {
125
                // No caching for this method allowed
126
 
127
                return $handler($request, $options)->then(
128
                    function (ResponseInterface $response) use ($request) {
129
                        if (!isset($this->safeMethods[$request->getMethod()])) {
130
                            // Invalidate cache after a call of non-safe method on the same URI
131
                            $response = $this->invalidateCache($request, $response);
132
                        }
133
 
1441 ariadna 134
                        return $response->withHeader(static::HEADER_CACHE_INFO, static::HEADER_CACHE_MISS);
1 efrain 135
                    }
136
                );
137
            }
138
 
1441 ariadna 139
            if ($request->hasHeader(static::HEADER_RE_VALIDATION)) {
1 efrain 140
                // It's a re-validation request, so bypass the cache!
1441 ariadna 141
                return $handler($request->withoutHeader(static::HEADER_RE_VALIDATION), $options);
1 efrain 142
            }
143
 
144
            // Retrieve information from request (Cache-Control)
145
            $reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control'));
146
            $onlyFromCache = $reqCacheControl->has('only-if-cached');
147
            $staleResponse = $reqCacheControl->has('max-stale')
148
                && $reqCacheControl->get('max-stale') === '';
149
            $maxStaleCache = $reqCacheControl->get('max-stale', null);
150
            $minFreshCache = $reqCacheControl->get('min-fresh', null);
151
 
152
            // If cache => return new FulfilledPromise(...) with response
153
            $cacheEntry = $this->cacheStorage->fetch($request);
154
            if ($cacheEntry instanceof CacheEntry) {
155
                $body = $cacheEntry->getResponse()->getBody();
156
                if ($body->tell() > 0) {
157
                    $body->rewind();
158
                }
159
 
160
                if ($cacheEntry->isFresh()
161
                    && ($minFreshCache === null || $cacheEntry->getStaleAge() + (int)$minFreshCache <= 0)
162
                ) {
163
                    // Cache HIT!
164
                    return new FulfilledPromise(
1441 ariadna 165
                        $cacheEntry->getResponse()->withHeader(static::HEADER_CACHE_INFO, static::HEADER_CACHE_HIT)
1 efrain 166
                    );
167
                } elseif ($staleResponse
168
                    || ($maxStaleCache !== null && $cacheEntry->getStaleAge() <= $maxStaleCache)
169
                ) {
170
                    // Staled cache!
171
                    return new FulfilledPromise(
1441 ariadna 172
                        $cacheEntry->getResponse()->withHeader(static::HEADER_CACHE_INFO, static::HEADER_CACHE_HIT)
1 efrain 173
                    );
174
                } elseif ($cacheEntry->hasValidationInformation() && !$onlyFromCache) {
175
                    // Re-validation header
176
                    $request = static::getRequestWithReValidationHeader($request, $cacheEntry);
177
 
178
                    if ($cacheEntry->staleWhileValidate()) {
179
                        static::addReValidationRequest($request, $this->cacheStorage, $cacheEntry);
180
 
181
                        return new FulfilledPromise(
182
                            $cacheEntry->getResponse()
1441 ariadna 183
                                ->withHeader(static::HEADER_CACHE_INFO, static::HEADER_CACHE_STALE)
1 efrain 184
                        );
185
                    }
186
                }
187
            } else {
188
                $cacheEntry = null;
189
            }
190
 
191
            if ($cacheEntry === null && $onlyFromCache) {
192
                // Explicit asking of a cached response => 504
193
                return new FulfilledPromise(
194
                    new Response(504)
195
                );
196
            }
197
 
198
            /** @var Promise $promise */
199
            $promise = $handler($request, $options);
200
 
201
            return $promise->then(
202
                function (ResponseInterface $response) use ($request, $cacheEntry) {
203
                    // Check if error and looking for a staled content
204
                    if ($response->getStatusCode() >= 500) {
205
                        $responseStale = static::getStaleResponse($cacheEntry);
206
                        if ($responseStale instanceof ResponseInterface) {
207
                            return $responseStale;
208
                        }
209
                    }
210
 
211
                    $update = false;
212
 
213
                    if ($response->getStatusCode() == 304 && $cacheEntry instanceof CacheEntry) {
214
                        // Not modified => cache entry is re-validate
215
                        /** @var ResponseInterface $response */
216
                        $response = $response
217
                            ->withStatus($cacheEntry->getResponse()->getStatusCode())
1441 ariadna 218
                            ->withHeader(static::HEADER_CACHE_INFO, static::HEADER_CACHE_HIT);
1 efrain 219
                        $response = $response->withBody($cacheEntry->getResponse()->getBody());
220
 
221
                        // Merge headers of the "304 Not Modified" and the cache entry
222
                        /**
223
                         * @var string $headerName
224
                         * @var string[] $headerValue
225
                         */
226
                        foreach ($cacheEntry->getOriginalResponse()->getHeaders() as $headerName => $headerValue) {
1441 ariadna 227
                            if (!$response->hasHeader($headerName) && $headerName !== static::HEADER_CACHE_INFO) {
1 efrain 228
                                $response = $response->withHeader($headerName, $headerValue);
229
                            }
230
                        }
231
 
232
                        $update = true;
233
                    } else {
1441 ariadna 234
                        $response = $response->withHeader(static::HEADER_CACHE_INFO, static::HEADER_CACHE_MISS);
1 efrain 235
                    }
236
 
237
                    return static::addToCache($this->cacheStorage, $request, $response, $update);
238
                },
239
                function ($reason) use ($cacheEntry) {
1441 ariadna 240
                    $response = static::getStaleResponse($cacheEntry);
241
                    if ($response instanceof ResponseInterface) {
242
                        return $response;
1 efrain 243
                    }
244
 
245
                    return new RejectedPromise($reason);
246
                }
247
            );
248
        };
249
    }
250
 
251
    /**
252
     * @param CacheStrategyInterface $cache
253
     * @param RequestInterface $request
254
     * @param ResponseInterface $response
255
     * @param bool $update cache
256
     * @return ResponseInterface
257
     */
258
    protected static function addToCache(
259
        CacheStrategyInterface $cache,
260
        RequestInterface $request,
261
        ResponseInterface $response,
262
        $update = false
263
    ) {
264
        $body = $response->getBody();
265
 
266
        // If the body is not seekable, we have to replace it by a seekable one
267
        if (!$body->isSeekable()) {
268
            $response = $response->withBody(
269
                \GuzzleHttp\Psr7\Utils::streamFor($body->getContents())
270
            );
271
        }
272
 
273
        if ($update) {
274
            $cache->update($request, $response);
275
        } else {
276
            $cache->cache($request, $response);
277
        }
278
 
279
        // always rewind back to the start otherwise other middlewares may get empty "content"
280
        if ($body->isSeekable()) {
281
            $response->getBody()->rewind();
282
        }
283
 
284
        return $response;
285
    }
286
 
287
    /**
288
     * @param RequestInterface       $request
289
     * @param CacheStrategyInterface $cacheStorage
290
     * @param CacheEntry             $cacheEntry
291
     *
292
     * @return bool if added
293
     */
294
    protected function addReValidationRequest(
295
        RequestInterface $request,
296
        CacheStrategyInterface &$cacheStorage,
297
        CacheEntry $cacheEntry
298
    ) {
299
        // Add the promise for revalidate
300
        if ($this->client !== null) {
301
            /** @var RequestInterface $request */
1441 ariadna 302
            $request = $request->withHeader(static::HEADER_RE_VALIDATION, '1');
1 efrain 303
            $this->waitingRevalidate[] = $this->client
304
                ->sendAsync($request)
305
                ->then(function (ResponseInterface $response) use ($request, &$cacheStorage, $cacheEntry) {
306
                    $update = false;
307
 
308
                    if ($response->getStatusCode() == 304) {
309
                        // Not modified => cache entry is re-validate
310
                        /** @var ResponseInterface $response */
311
                        $response = $response->withStatus($cacheEntry->getResponse()->getStatusCode());
312
                        $response = $response->withBody($cacheEntry->getResponse()->getBody());
313
 
314
                        // Merge headers of the "304 Not Modified" and the cache entry
315
                        foreach ($cacheEntry->getResponse()->getHeaders() as $headerName => $headerValue) {
316
                            if (!$response->hasHeader($headerName)) {
317
                                $response = $response->withHeader($headerName, $headerValue);
318
                            }
319
                        }
320
 
321
                        $update = true;
322
                    }
323
 
324
                    static::addToCache($cacheStorage, $request, $response, $update);
325
                });
326
 
327
            return true;
328
        }
329
 
330
        return false;
331
    }
332
 
333
    /**
334
     * @param CacheEntry|null $cacheEntry
335
     *
336
     * @return null|ResponseInterface
337
     */
1441 ariadna 338
    protected static function getStaleResponse(?CacheEntry $cacheEntry = null)
1 efrain 339
    {
340
        // Return staled cache entry if we can
341
        if ($cacheEntry instanceof CacheEntry && $cacheEntry->serveStaleIfError()) {
342
            return $cacheEntry->getResponse()
1441 ariadna 343
                ->withHeader(static::HEADER_CACHE_INFO, static::HEADER_CACHE_STALE);
1 efrain 344
        }
345
 
346
        return;
347
    }
348
 
349
    /**
350
     * @param RequestInterface $request
351
     * @param CacheEntry       $cacheEntry
352
     *
353
     * @return RequestInterface
354
     */
1441 ariadna 355
    protected static function getRequestWithReValidationHeader(RequestInterface $request, ?CacheEntry $cacheEntry)
1 efrain 356
    {
357
        if ($cacheEntry->getResponse()->hasHeader('Last-Modified')) {
358
            $request = $request->withHeader(
359
                'If-Modified-Since',
360
                $cacheEntry->getResponse()->getHeader('Last-Modified')
361
            );
362
        }
363
        if ($cacheEntry->getResponse()->hasHeader('Etag')) {
364
            $request = $request->withHeader(
365
                'If-None-Match',
366
                $cacheEntry->getResponse()->getHeader('Etag')
367
            );
368
        }
369
 
370
        return $request;
371
    }
372
 
373
    /**
374
     * @param CacheStrategyInterface|null $cacheStorage
375
     *
376
     * @return CacheMiddleware the Middleware for Guzzle HandlerStack
377
     *
378
     * @deprecated Use constructor => `new CacheMiddleware()`
379
     */
1441 ariadna 380
    public static function getMiddleware(?CacheStrategyInterface $cacheStorage = null)
1 efrain 381
    {
382
        return new self($cacheStorage);
383
    }
384
 
385
    /**
386
     * @param RequestInterface $request
387
     *
388
     * @param ResponseInterface $response
389
     *
390
     * @return ResponseInterface
391
     */
392
    private function invalidateCache(RequestInterface $request, ResponseInterface $response)
393
    {
394
        foreach (array_keys($this->httpMethods) as $method) {
395
            $this->cacheStorage->delete($request->withMethod($method));
396
        }
397
 
1441 ariadna 398
        return $response->withHeader(static::HEADER_INVALIDATION, true);
1 efrain 399
    }
400
}