CacheHandler   B
last analyzed

Complexity

Total Complexity 52

Size/Duplication

Total Lines 545
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 52
lcom 1
cbo 9
dl 0
loc 545
ccs 0
cts 204
cp 0
rs 7.44
c 0
b 0
f 0

33 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A setHandler() 0 4 1
A setCacheProvider() 0 4 1
A setOptions() 0 4 1
A setLogger() 0 4 1
A setLogTemplate() 0 4 1
A getLogTemplate() 0 8 2
A getDefaultHandler() 0 4 1
A getDefaultOptions() 0 14 1
A __invoke() 0 8 2
A cache() 0 16 3
A request() 0 26 3
A lastRequestWasFetchedFromCache() 0 4 1
A fetch() 0 9 2
A fetchBundle() 0 16 4
A store() 0 14 2
A buildCacheBundle() 0 7 1
A filter() 0 5 2
A checkMethod() 0 5 1
A shouldCacheRequest() 0 4 2
A shouldCacheResponse() 0 4 2
A getKey() 0 8 1
A invokeDefault() 0 4 1
A getDefaultLogLevel() 0 4 1
A setLogLevel() 0 4 1
A getLogLevel() 0 12 3
A log() 0 7 2
A logStoredBundle() 0 5 1
A logFetchedBundle() 0 5 1
A prepareTemplate() 0 10 2
A getLogMessage() 0 12 1
A getStoredLogMessage() 0 4 1
A getFetchedLogMessage() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like CacheHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CacheHandler, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Concat\Http\Handler;
4
5
use Concat\Cache\Adapter\AdapterFactory;
6
use Concat\Cache\CacheInterface;
7
use Doctrine\Common\Cache\FilesystemCache;
8
use GuzzleHttp\MessageFormatter;
9
use GuzzleHttp\Promise\FulfilledPromise;
10
use GuzzleHttp\Promise\PromiseInterface;
11
use GuzzleHttp\Psr7\Request;
12
use GuzzleHttp\Psr7\Response;
13
use Psr\Log\LoggerInterface;
14
use Psr\Log\LogLevel;
15
use RuntimeException;
16
17
/**
18
 * Guzzle handler used to cache responses.
19
 */
20
class CacheHandler
21
{
22
23
    /**
24
     * @var \Concat\Cache\CacheInterface Cache provider.
25
     */
26
    protected $cache;
27
28
    /**
29
     * @var callable Default handler used to send response.
30
     */
31
    protected $handler;
32
33
    /**
34
     * @var \Psr\Log\LoggerInterface PSR-3 compliant logger.
35
     */
36
    protected $logger;
37
38
    /**
39
     * @var string|callable Constant or callable that accepts a Response.
40
     */
41
    protected $logLevel;
42
43
    /**
44
     * @var string Log template.
45
     */
46
    protected $logTemplate;
47
48
    /**
49
     * @var array Configuration options.
50
     */
51
    protected $options;
52
53
    /**
54
     *  @var bool Whether the most recent request was fetched from the cache.
55
     */
56
    private $fetchedFromCache = false;
57
58
    /**
59
     * Constructs a new cache handler.
60
     *
61
     * @param object $cache Cache provider.
62
     * @param callable $handler Default handler used to send response.
63
     * @param array $options Configuration options.
64
     */
65
    public function __construct($cache, $handler = null, array $options = [])
66
    {
67
        $handler = $handler ?: $this->getDefaultHandler();
68
69
        $this->setHandler($handler);
70
        $this->setCacheProvider($cache);
71
        $this->setOptions($options);
72
    }
73
74
    /**
75
     * Sets the fallback handler to use when the cache is invalid.
76
     *
77
     * @param callable $handler
78
     *
79
     * @codeCoverageIgnore
80
     */
81
    public function setHandler(callable $handler)
82
    {
83
        $this->handler = $handler;
84
    }
85
86
    /**
87
     * Sets the cache provider.
88
     *
89
     * @param object $cache
90
     */
91
    public function setCacheProvider($cache)
92
    {
93
        $this->cache = AdapterFactory::get($cache);
94
    }
95
96
    /**
97
     * Resets the options, merged with default values.
98
     *
99
     * @param array $options
100
     */
101
    public function setOptions(array $options)
102
    {
103
        $this->options = array_merge($this->getDefaultOptions(), $options);
104
    }
105
106
    /**
107
     * Sets the logger.
108
     *
109
     * @param LoggerInterface $logger
110
     */
111
    public function setLogger(LoggerInterface $logger)
112
    {
113
        $this->logger = $logger;
114
    }
115
116
    /**
117
     * Sets the template to use when logging cache events.
118
     *
119
     * @param string $logTemplate
120
     */
121
    public function setLogTemplate($logTemplate)
122
    {
123
        $this->logTemplate = $logTemplate;
124
    }
125
126
    /**
127
     * Returns a defined log template, or a default template otherwise.
128
     *
129
     * @return string Log template.
130
     */
131
    protected function getLogTemplate()
132
    {
133
        if (is_null($this->logTemplate)) {
134
            return MessageFormatter::SHORT . " {event} (expires in {expires}s)";
135
        }
136
137
        return $this->logTemplate;
138
    }
139
140
    /**
141
     * Returns the default handler, used if a handler is not set.
142
     *
143
     * @return callable
144
     * @codeCoverageIgnore
145
     */
146
    protected function getDefaultHandler()
147
    {
148
        return \GuzzleHttp\choose_handler();
0 ignored issues
show
Deprecated Code introduced by
The function GuzzleHttp\choose_handler() has been deprecated with message: choose_handler will be removed in guzzlehttp/guzzle:8.0. Use Utils::chooseHandler instead.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
149
    }
150
151
    /**
152
     * Returns the default confiration options.
153
     *
154
     * @return array The default configuration options.
155
     */
156
    protected function getDefaultOptions()
157
    {
158
        return [
159
160
            // HTTP methods that should be cached
161
            'methods' => ['GET', 'HEAD', 'OPTIONS'],
162
163
            // Time in seconds to cache the response for
164
            'expire'  => 30,
165
166
            // Accepts a request and returns true if it should be cached
167
            'filter'  => null,
168
        ];
169
    }
170
171
    /**
172
     * Called when a request is made on the client.
173
     *
174
     * @return PromiseInterface
175
     */
176
    public function __invoke(Request $request, array $options)
177
    {
178
        if ($this->shouldCacheRequest($request)) {
179
            return $this->cache($request, $options);
180
        }
181
182
        return $this->invokeDefault($request, $options);
183
    }
184
185
    /**
186
     * Attempts to fetch, otherwise promises to cache a response when the
187
     * default handler fulfills its promise.
188
     *
189
     * @param Request $request The request to cache.
190
     * @param array $options Configuration options.
191
     *
192
     * @return PromiseInterface
193
     */
194
    protected function cache(Request $request, array $options)
195
    {
196
        $key = $this->getKey($request, $options);
197
198
        if ($this->cache->contains($key)) {
199
            $response = $this->fetch($request, $key);
200
201
            // Return the cached response if fetch was successful.
202
            if ($response) {
203
                $this->fetchedFromCache = true;
204
                return new FulfilledPromise($response);
205
            }
206
        }
207
208
        return $this->request($request, $options, $key);
209
    }
210
211
    /**
212
     * Makes the request and stores it in the cache if required to.
213
     *
214
     * @param Request $request
215
     * @param array   $options
216
     * @param string  $key
217
     *
218
     * @return PromiseInterface
219
     */
220
    protected function request(Request $request, array $options, $key)
221
    {
222
        $this->fetchedFromCache = false;
223
224
        // Make the request using the default handler.
225
        $promise = $this->invokeDefault($request, $options);
226
227
        // Don't store if the expire time isn't positive.
228
        if ($this->options['expire'] <= 0) {
229
            return $promise;
230
        }
231
232
        // Promise to store the response once the default promise is fulfilled.
233
        return $promise->then(function ($response) use ($request, $key) {
234
            if ($this->shouldCacheResponse($response)) {
235
236
                // Evaluate the content stream so that it can be cached.
237
                $stream = new CachedStream((string) $response->getBody());
238
                $response = $response->withBody($stream);
239
240
                $this->store($request, $response, $key);
241
            }
242
243
            return $response;
244
        });
245
    }
246
247
    /**
248
     * Returns true if the most recent request was fetched from the cache, or
249
     * false if the request was made (regardless of whether it was then cached).
250
     *
251
     * @return bool
252
     */
253
    public function lastRequestWasFetchedFromCache()
254
    {
255
        return $this->fetchedFromCache;
256
    }
257
258
    /**
259
     * Attempts to fetch a response bundle from the cache for the given key.
260
     *
261
     * @param Request $request
262
     * @param string $key
263
     *
264
     * @return Response|null A response null if invalid.
265
     */
266
    protected function fetch(Request $request, $key)
267
    {
268
        $bundle = $this->fetchBundle($key);
269
270
        if ($bundle) {
271
            $this->logFetchedBundle($request, $bundle);
272
            return $bundle['response'];
273
        }
274
    }
275
276
    /**
277
     * Fetches a response bundle from the cache for a given key.
278
     *
279
     * @param string $key The key to fetch.
280
     *
281
     * @return array|null Bundle from cache or null if expired.
282
     */
283
    protected function fetchBundle($key)
284
    {
285
        $bundle = $this->cache->fetch($key);
286
287
        if ($bundle === false) {
288
            throw new RuntimeException("Failed to fetch response from cache");
289
        }
290
        if ($bundle === null || time() >= $bundle['expires']) {
291
            // Delete expired entries so that they don't trigger 'contains'.
292
            $this->cache->delete($key);
293
294
            return;
295
        }
296
297
        return $bundle;
298
    }
299
300
    /**
301
     * Builds and stores a cache bundle.
302
     *
303
     * @param Request $request
304
     * @param Response $response
305
     * @param string $key
306
     *
307
     * @throws RuntimeException if it fails to store the response in the cache.
308
     */
309
    protected function store(Request $request, Response $response, $key)
310
    {
311
        $bundle = $this->buildCacheBundle($response);
312
313
        // Store the bundle in the cache
314
        $save = $this->cache->store($key, $bundle, $this->options['expire']);
315
316
        if ($save === false) {
317
            throw new RuntimeException("Failed to store response to cache");
318
        }
319
320
        // Log that it has been stored
321
        $this->logStoredBundle($request, $bundle);
322
    }
323
324
    /**
325
     * Builds a cache bundle using a given response.
326
     *
327
     * @param Response $response
328
     *
329
     * @return array The response bundle to cache.
330
     */
331
    protected function buildCacheBundle(Response $response)
332
    {
333
        return [
334
            'response' => $response,
335
            'expires'  => time() + $this->options['expire'],
336
        ];
337
    }
338
339
    /**
340
     * Filters the request using a configured filter to determine if it should
341
     * be cached.
342
     *
343
     * @param Request $request The request to filter.
344
     *
345
     * @return bool true if should be cached, false otherwise.
346
     */
347
    protected function filter(Request $request)
348
    {
349
        $filter = $this->options['filter'];
350
        return ! is_callable($filter) || call_user_func($filter, $request);
351
    }
352
353
    /**
354
     * Checks the method of the request to determine if it should be cached.
355
     *
356
     * @param Request $request The request to check.
357
     *
358
     * @return bool true if should be cached, false otherwise.
359
     */
360
    protected function checkMethod(Request $request)
361
    {
362
        $methods = (array) $this->options['methods'];
363
        return in_array($request->getMethod(), $methods);
364
    }
365
366
    /**
367
     * Returns true if the given request should be cached.
368
     *
369
     * @param Request $request The request to check.
370
     *
371
     * @return bool true if the request should be cached, false otherwise.
372
     */
373
    private function shouldCacheRequest(Request $request)
374
    {
375
        return $this->checkMethod($request) && $this->filter($request);
376
    }
377
378
    /**
379
     * Determines if a response should be cached.
380
     *
381
     * @param Response $response
382
     */
383
    protected function shouldCacheResponse(Response $response)
384
    {
385
        return $response && $response->getStatusCode() < 400;
386
    }
387
388
    /**
389
     * Generates the cache key for the given request and request options. The
390
     * namespace should be set on the cache provider.
391
     *
392
     * @param Request $request The request to generate a key for.
393
     * @param array $options Configuration options.
394
     *
395
     * @return string The cache key
396
     */
397
    protected function getKey(Request $request, array $options)
398
    {
399
        return join(":", [
400
            $request->getMethod(),
401
            $request->getUri(),
402
            md5(json_encode($options)),
403
        ]);
404
    }
405
406
    /**
407
     * Invokes the default handler to produce a promise.
408
     *
409
     * @param Request $request
410
     * @param array $options
411
     *
412
     * @return PromiseInterface
413
     */
414
    protected function invokeDefault(Request $request, array $options)
415
    {
416
        return call_user_func($this->handler, $request, $options);
417
    }
418
419
    /**
420
     * Returns the default log level to use when logging response bundles.
421
     *
422
     * @return string LogLevel
423
     */
424
    protected function getDefaultLogLevel()
425
    {
426
        return LogLevel::DEBUG;
427
    }
428
429
    /**
430
     * Sets the log level to use, which can be either a string or a callable
431
     * that accepts a response (which could be null). A log level could also
432
     * be null, which indicates that the default log level should be used.
433
     *
434
     * @param string|callable|null
435
     */
436
    public function setLogLevel($logLevel)
437
    {
438
        $this->logLevel = $logLevel;
439
    }
440
441
    /**
442
     * Returns a log level for a given response.
443
     *
444
     * @param Response $response The response being logged.
445
     *
446
     * @return string LogLevel
447
     */
448
    protected function getLogLevel(Response $response)
449
    {
450
        if (is_null($this->logLevel)) {
451
            return $this->getDefaultLogLevel();
452
        }
453
454
        if (is_callable($this->logLevel)) {
455
            return call_user_func($this->logLevel, $response);
456
        }
457
458
        return (string) $this->logLevel;
459
    }
460
461
    /**
462
     * Convenient internal logger entry point.
463
     *
464
     * @param string $message
465
     * @param array $bundle
466
     */
467
    private function log($message, array $bundle)
468
    {
469
        if (isset($this->logger)) {
470
            $level = $this->getLogLevel($bundle['response']);
471
            $this->logger->log($level, $message, $bundle);
472
        }
473
    }
474
475
    /**
476
     * Logs that a bundle has been stored in the cache.
477
     *
478
     * @param Request $request The request.
479
     * @param array $bundle The stored response bundle.
480
     */
481
    protected function logStoredBundle(Request $request, array $bundle)
482
    {
483
        $message = $this->getStoredLogMessage($request, $bundle);
484
        $this->log($message, $bundle);
485
    }
486
487
    /**
488
     * Logs that a bundle has been fetched from the cache.
489
     *
490
     * @param Request $request The request that produced the response.
491
     * @param array $bundle The fetched response bundle.
492
     */
493
    protected function logFetchedBundle(Request $request, array $bundle)
494
    {
495
        $message = $this->getFetchedLogMessage($request, $bundle);
496
        $this->log($message, $bundle);
497
    }
498
499
    /**
500
     * Prepares a log template with optional extra fields.
501
     *
502
     * @param array $extras
503
     *
504
     * @return string $template
505
     */
506
    protected function prepareTemplate(array $extras)
507
    {
508
        $template = $this->getLogTemplate();
509
510
        foreach ($extras as $key => $value) {
511
            $template = str_replace('{' . $key . '}', $value, $template);
512
        }
513
514
        return $template;
515
    }
516
517
    /**
518
     * Formats a request and response as a log message.
519
     *
520
     * @param Request $request
521
     * @param array $bundle
522
     * @param string $event
523
     *
524
     * @return string The formatted message.
525
     */
526
    protected function getLogMessage(Request $request, array $bundle, $event)
527
    {
528
        $template = $this->prepareTemplate([
529
            'event'   => $event,
530
            'expires' => $bundle['expires'] - time(),
531
        ]);
532
533
        $response  = $bundle['response'];
534
        $formatter = new MessageFormatter($template);
535
536
        return $formatter->format($request, $response);
537
    }
538
539
    /**
540
     * Returns the log message for when a bundle is stored in the cache.
541
     *
542
     * @param Request $request The request that produced the response.
543
     * @param array $bundle The stored response bundle.
544
     *
545
     * @return string The log message.
546
     */
547
    protected function getStoredLogMessage(Request $request, array $bundle)
548
    {
549
        return $this->getLogMessage($request, $bundle, 'stored in cache');
550
    }
551
552
    /**
553
     * Returns the log message for when a bundle is fetched from the cache.
554
     *
555
     * @param Request $request The request that produced the response.
556
     * @param array $bundle The stored response bundle.
557
     *
558
     * @return string The log message.
559
     */
560
    protected function getFetchedLogMessage(Request $request, array $bundle)
561
    {
562
        return $this->getLogMessage($request, $bundle, 'fetched from cache');
563
    }
564
}
565