Completed
Push — master ( 01ea43...0e4997 )
by David
02:07
created

CachePlugin::isCacheable()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5.3906

Importance

Changes 4
Bugs 1 Features 1
Metric Value
c 4
b 1
f 1
dl 0
loc 14
ccs 6
cts 8
cp 0.75
rs 8.8571
cc 5
eloc 8
nc 4
nop 1
crap 5.3906
1
<?php
2
3
namespace Http\Client\Plugin;
4
5
use Http\Client\Tools\Promise\FulfilledPromise;
6
use Psr\Cache\CacheItemPoolInterface;
7
use Psr\Http\Message\RequestInterface;
8
use Psr\Http\Message\ResponseInterface;
9
10
/**
11
 * Allow for caching a response.
12
 *
13
 * @author Tobias Nyholm <[email protected]>
14
 */
15
class CachePlugin implements Plugin
16
{
17
    /**
18
     * @var CacheItemPoolInterface
19
     */
20
    private $pool;
21
22
    /**
23
     * Default time to store object in cache. This value is used if CachePlugin::respectCacheHeaders is false or
24
     * if cache headers are missing.
25
     *
26
     * @var int
27
     */
28
    private $defaultTtl;
29
30
    /**
31
     * Look at the cache headers to know whether this response may be cached and to 
32
     * decide how it can be cached.
33
     *
34
     * @var bool Defaults to true
35
     */
36
    private $respectCacheHeaders;
37
38
    /**
39
     * Available options are
40
     *  - respect_cache_headers: Whether to look at the cache directives or ignore them.
41
     * 
42
     * @param CacheItemPoolInterface $pool
43
     * @param array                  $options
44
     */
45 6
    public function __construct(CacheItemPoolInterface $pool, array $options = [])
46
    {
47 6
        $this->pool = $pool;
48 6
        $this->defaultTtl = isset($options['default_ttl']) ? $options['default_ttl'] : null;
49 6
        $this->respectCacheHeaders = isset($options['respect_cache_headers']) ? $options['respect_cache_headers'] : true;
50 6
    }
51
52
    /**
53
     * {@inheritdoc}
54
     */
55 4
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
56
    {
57 4
        $method = strtoupper($request->getMethod());
58
59
        // if the request not is cachable, move to $next
60 4
        if ($method !== 'GET' && $method !== 'HEAD') {
61 1
            return $next($request);
62
        }
63
64
        // If we can cache the request
65 3
        $key = $this->createCacheKey($request);
66 3
        $cacheItem = $this->pool->getItem($key);
67
68 3
        if ($cacheItem->isHit()) {
69
            // return cached response
70
            return new FulfilledPromise($cacheItem->get());
71
        }
72
73 3
        return $next($request)->then(function (ResponseInterface $response) use ($cacheItem) {
74 3
            if ($this->isCacheable($response)) {
75 2
                $cacheItem->set($response)
76 2
                    ->expiresAfter($this->getMaxAge($response));
77 2
                $this->pool->save($cacheItem);
78 2
            }
79
80 3
            return $response;
81 3
        });
82
    }
83
84
    /**
85
     * Verify that we can cache this response.
86
     *
87
     * @param ResponseInterface $response
88
     *
89
     * @return bool
90
     */
91 3
    protected function isCacheable(ResponseInterface $response)
92
    {
93 3
        if (!in_array($response->getStatusCode(), [200, 203, 300, 301, 302, 404, 410])) {
94 1
            return false;
95
        }
96 2
        if (!$this->respectCacheHeaders) {
97
            return true;
98
        }
99 2
        if ($this->getCacheControlDirective($response, 'no-store') || $this->getCacheControlDirective($response, 'private')) {
100
            return false;
101
        }
102
103 2
        return true;
104
    }
105
106
    /**
107
     * Get the value of a parameter in the cache control header.
108
     *
109
     * @param ResponseInterface $response
110
     * @param string            $name     The field of Cache-Control to fetch
111
     *
112
     * @return bool|string The value of the directive, true if directive without value, false if directive not present.
113
     */
114 2
    private function getCacheControlDirective(ResponseInterface $response, $name)
115
    {
116 2
        $headers = $response->getHeader('Cache-Control');
117 2
        foreach ($headers as $header) {
118 1
            if (preg_match(sprintf('|%s=?([0-9]+)?|i', $name), $header, $matches)) {
119
120
                // return the value for $name if it exists
121 1
                if (isset($matches[1])) {
122 1
                    return $matches[1];
123
                }
124
125
                return true;
126
            }
127 2
        }
128
129 2
        return false;
130
    }
131
132
    /**
133
     * @param RequestInterface $request
134
     *
135
     * @return string
136
     */
137 3
    private function createCacheKey(RequestInterface $request)
138
    {
139 3
        return md5($request->getMethod().' '.$request->getUri());
140
    }
141
142
    /**
143
     * Get a ttl in seconds. It could return null if we do not respect cache headers and got no defaultTtl.
144
     *
145
     * @param ResponseInterface $response
146
     *
147
     * @return int|null
148
     */
149 2
    private function getMaxAge(ResponseInterface $response)
150
    {
151 2
        if (!$this->respectCacheHeaders) {
152
            return $this->defaultTtl;
153
        }
154
155
        // check for max age in the Cache-Control header
156 2
        $maxAge = $this->getCacheControlDirective($response, 'max-age');
157 2
        if (!is_bool($maxAge)) {
158 1
            $ageHeaders = $response->getHeader('Age');
159 1
            foreach ($ageHeaders as $age) {
160 1
                return $maxAge - ((int) $age);
161
            }
162
163
            return $maxAge;
164
        }
165
166
        // check for ttl in the Expires header
167 1
        $headers = $response->getHeader('Expires');
168 1
        foreach ($headers as $header) {
169
            return (new \DateTime($header))->getTimestamp() - (new \DateTime())->getTimestamp();
170 1
        }
171
172 1
        return $this->defaultTtl;
173
    }
174
}
175