Passed
Push — master ( 59198c...cdc699 )
by Kevin
02:26
created

PrivateCacheStrategy::delete()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Kevinrob\GuzzleCache\Strategy;
4
5
use Kevinrob\GuzzleCache\CacheEntry;
6
use Kevinrob\GuzzleCache\KeyValueHttpHeader;
7
use Kevinrob\GuzzleCache\Storage\CacheStorageInterface;
8
use Kevinrob\GuzzleCache\Storage\VolatileRuntimeStorage;
9
use Psr\Http\Message\RequestInterface;
10
use Psr\Http\Message\ResponseInterface;
11
12
/**
13
 * This strategy represents a "private" HTTP client.
14
 * Pay attention to share storage between application with caution!
15
 *
16
 * For example, a response with cache-control header "private, max-age=60"
17
 * will be cached by this strategy.
18
 *
19
 * The rules applied are from RFC 7234.
20
 *
21
 * @see https://tools.ietf.org/html/rfc7234
22
 */
23
class PrivateCacheStrategy implements CacheStrategyInterface
24
{
25
    /**
26
     * @var CacheStorageInterface
27
     */
28
    protected $storage;
29
30
    /**
31
     * @var int[]
32
     */
33
    protected $statusAccepted = [
34
        200 => 200,
35
        203 => 203,
36
        204 => 204,
37
        300 => 300,
38
        301 => 301,
39
        404 => 404,
40
        405 => 405,
41
        410 => 410,
42
        414 => 414,
43
        418 => 418,
44
        501 => 501,
45
    ];
46
47
    /**
48
     * @var string[]
49
     */
50
    protected $ageKey = [
51
        'max-age',
52
    ];
53
54 41
    public function __construct(CacheStorageInterface $cache = null)
55
    {
56 41
        $this->storage = $cache !== null ? $cache : new VolatileRuntimeStorage();
57 41
    }
58
59
    /**
60
     * @param RequestInterface $request
61
     * @param ResponseInterface $response
62
     * @return CacheEntry|null entry to save, null if can't cache it
63
     */
64 31
    protected function getCacheObject(RequestInterface $request, ResponseInterface $response)
65
    {
66 31
        if (!isset($this->statusAccepted[$response->getStatusCode()])) {
67
            // Don't cache it
68
            return;
69
        }
70
71 31
        $cacheControl = new KeyValueHttpHeader($response->getHeader('Cache-Control'));
72 31
        $varyHeader = new KeyValueHttpHeader($response->getHeader('Vary'));
73
74 31
        if ($varyHeader->has('*')) {
75
            // This will never match with a request
76 1
            return;
77
        }
78
79 30
        if ($cacheControl->has('no-store')) {
80
            // No store allowed (maybe some sensitives data...)
81 1
            return;
82
        }
83
84 29
        if ($cacheControl->has('no-cache')) {
85
            // Stale response see RFC7234 section 5.2.1.4
86 1
            $entry = new CacheEntry($request, $response, new \DateTime('-1 seconds'));
87
88 1
            return $entry->hasValidationInformation() ? $entry : null;
89
        }
90
91 28
        foreach ($this->ageKey as $key) {
92 28
            if ($cacheControl->has($key)) {
93 19
                return new CacheEntry(
94 19
                    $request,
95 19
                    $response,
96 19
                    new \DateTime('+'.(int) $cacheControl->get($key).'seconds')
97 19
                );
98
            }
99 12
        }
100
101 9
        if ($response->hasHeader('Expires')) {
102 5
            $expireAt = \DateTime::createFromFormat(\DateTime::RFC1123, $response->getHeaderLine('Expires'));
103 5
            if ($expireAt !== false) {
104 5
                return new CacheEntry(
105 5
                    $request,
106 5
                    $response,
107
                    $expireAt
108 5
                );
109
            }
110
        }
111
112 4
        return new CacheEntry($request, $response, new \DateTime('-1 seconds'));
113
    }
114
115
    /**
116
     * Generate a key for the response cache.
117
     *
118
     * @param RequestInterface   $request
119
     * @param null|KeyValueHttpHeader $varyHeaders The vary headers which should be honoured by the cache (optional)
120
     *
121
     * @return string
122
     */
123 32
    protected function getCacheKey(RequestInterface $request, KeyValueHttpHeader $varyHeaders = null)
124
    {
125 32
        if (!$varyHeaders) {
126 32
            return hash('sha256', $request->getMethod().$request->getUri());
127
        }
128
129 3
        $cacheHeaders = [];
130
131 3
        foreach ($varyHeaders as $key => $value) {
132 3
            if ($request->hasHeader($key)) {
133 3
                $cacheHeaders[$key] = $request->getHeader($key);
134 3
            }
135 3
        }
136
137 3
        return hash('sha256', $request->getMethod().$request->getUri().json_encode($cacheHeaders));
138
    }
139
140
    /**
141
     * Return a CacheEntry or null if no cache.
142
     *
143
     * @param RequestInterface $request
144
     *
145
     * @return CacheEntry|null
146
     */
147 32
    public function fetch(RequestInterface $request)
148
    {
149
        /** @var int|null $maxAge */
150 32
        $maxAge = null;
151
152 32
        if ($request->hasHeader('Cache-Control')) {
153 7
            $reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control'));
154 7
            if ($reqCacheControl->has('no-cache')) {
155
                // Can't return cache
156 1
                return null;
157
            }
158
159 6
            $maxAge = $reqCacheControl->get('max-age', null);
160 32
        } elseif ($request->hasHeader('Pragma')) {
161 1
            $pragma = new KeyValueHttpHeader($request->getHeader('Pragma'));
162 1
            if ($pragma->has('no-cache')) {
163
                // Can't return cache
164 1
                return null;
165
            }
166
        }
167
168 32
        $cache = $this->storage->fetch($this->getCacheKey($request));
169 32
        if ($cache !== null) {
170 28
            $varyHeaders = $cache->getVaryHeaders();
171
172
            // vary headers exist from a previous response, check if we have a cache that matches those headers
173 28
            if (!$varyHeaders->isEmpty()) {
174 3
                $cache = $this->storage->fetch($this->getCacheKey($request, $varyHeaders));
175
176 3
                if (!$cache) {
177 2
                    return null;
178
                }
179 3
            }
180
181 28
            if ((string)$cache->getOriginalRequest()->getUri() !== (string)$request->getUri()) {
182
                return null;
183
            }
184
185 28
            if ($maxAge !== null) {
186 1
                if ($cache->getAge() > $maxAge) {
187
                    // Cache entry is too old for the request requirements!
188 1
                    return null;
189
                }
190
            }
191
192 28
            if (!$cache->isVaryEquals($request)) {
193
                return null;
194
            }
195 28
        }
196
197 32
        return $cache;
198
    }
199
200
    /**
201
     * @param RequestInterface  $request
202
     * @param ResponseInterface $response
203
     *
204
     * @return bool true if success
205
     */
206 32
    public function cache(RequestInterface $request, ResponseInterface $response)
207
    {
208 32
        $reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control'));
209 32
        if ($reqCacheControl->has('no-store')) {
210
            // No caching allowed
211 1
            return false;
212
        }
213
214 32
        $cacheObject = $this->getCacheObject($request, $response);
215 32
        if ($cacheObject !== null) {
216
            // store the cache against the URI-only key
217 29
            $success = $this->storage->save(
218 29
                $this->getCacheKey($request),
219
                $cacheObject
220 29
            );
221
222 29
            $varyHeaders = $cacheObject->getVaryHeaders();
223
224 29
            if (!$varyHeaders->isEmpty()) {
225
                // also store the cache against the vary headers based key
226 3
                $success = $this->storage->save(
227 3
                    $this->getCacheKey($request, $varyHeaders),
228
                    $cacheObject
229 3
                );
230 3
            }
231
232 29
            return $success;
233
        }
234
235 3
        return false;
236
    }
237
238
    /**
239
     * @param RequestInterface $request
240
     * @param ResponseInterface $response
241
     *
242
     * @return bool true if success
243
     */
244 5
    public function update(RequestInterface $request, ResponseInterface $response)
245
    {
246 5
        return $this->cache($request, $response);
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     */
252 2
    public function delete(RequestInterface $request)
253
    {
254 2
        return $this->storage->delete($this->getCacheKey($request));
255
    }
256
}
257