Proyectos de Subversion Moodle

Rev

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