Completed
Branch wip-51 (77acd0)
by Kevin
03:09
created

PrivateCacheStrategy   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 190
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 16
Bugs 2 Features 2
Metric Value
wmc 27
c 16
b 2
f 2
lcom 1
cbo 6
dl 0
loc 190
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 2
C getCacheObject() 0 50 10
A getCacheKey() 0 7 1
D fetch() 0 41 10
A cache() 0 18 3
A update() 0 4 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 represent 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
    public function __construct(CacheStorageInterface $cache = null)
55
    {
56
        $this->storage = $cache !== null ? $cache : new VolatileRuntimeStorage();
57
    }
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
    protected function getCacheObject(RequestInterface $request, ResponseInterface $response)
65
    {
66
        if (!isset($this->statusAccepted[$response->getStatusCode()])) {
67
            // Don't cache it
68
            return;
69
        }
70
71
        $cacheControl = new KeyValueHttpHeader($response->getHeader('Cache-Control'));
72
        $varyHeader = new KeyValueHttpHeader($response->getHeader('Vary'));
73
74
        if ($varyHeader->has('*')) {
75
            // This will never match with a request
76
            return;
77
        }
78
79
        if ($cacheControl->has('no-store')) {
80
            // No store allowed (maybe some sensitives data...)
81
            return;
82
        }
83
84
        if ($cacheControl->has('no-cache')) {
85
            // Stale response see RFC7234 section 5.2.1.4
86
            $entry = new CacheEntry($request, $response, new \DateTime('-1 seconds'));
87
88
            return $entry->hasValidationInformation() ? $entry : null;
89
        }
90
91
        foreach ($this->ageKey as $key) {
92
            if ($cacheControl->has($key)) {
93
                return new CacheEntry(
94
                    $request,
95
                    $response,
96
                    new \DateTime('+'.(int) $cacheControl->get($key).'seconds')
97
                );
98
            }
99
        }
100
101
        if ($response->hasHeader('Expires')) {
102
            $expireAt = \DateTime::createFromFormat(\DateTime::RFC1123, $response->getHeaderLine('Expires'));
103
            if ($expireAt !== false) {
104
                return new CacheEntry(
105
                    $request,
106
                    $response,
107
                    $expireAt
108
                );
109
            }
110
        }
111
112
        return new CacheEntry($request, $response, new \DateTime('-1 seconds'));
113
    }
114
115
    /**
116
     * @param RequestInterface $request
117
     *
118
     * @return string
119
     */
120
    protected function getCacheKey(RequestInterface $request)
121
    {
122
        return hash(
123
            'sha256',
124
            $request->getMethod().$request->getUri()
125
        );
126
    }
127
128
    /**
129
     * Return a CacheEntry or null if no cache.
130
     *
131
     * @param RequestInterface $request
132
     *
133
     * @return CacheEntry|null
134
     */
135
    public function fetch(RequestInterface $request)
136
    {
137
        /** @var int|null $maxAge */
138
        $maxAge = null;
139
140
        if ($request->hasHeader('Cache-Control')) {
141
            $reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control'));
142
            if ($reqCacheControl->has('no-cache')) {
143
                // Can't return cache
144
                return null;
145
            }
146
147
            $maxAge = $reqCacheControl->get('max-age', null);
148
        } elseif ($request->hasHeader('Pragma')) {
149
            $pragma = new KeyValueHttpHeader($request->getHeader('Pragma'));
150
            if ($pragma->has('no-cache')) {
151
                // Can't return cache
152
                return null;
153
            }
154
        }
155
156
        $cache = $this->storage->fetch($this->getCacheKey($request));
157
        if ($cache !== null) {
158
            if ((string)$cache->getOriginalRequest()->getUri() !== (string)$request->getUri()) {
159
                return null;
160
            }
161
162
            if ($maxAge !== null) {
163
                if ($cache->getAge() > $maxAge) {
164
                    // Cache entry is too old for the request requirements!
165
                    return null;
166
                }
167
            }
168
169
            if (!$cache->isVaryEquals($request)) {
170
                return null;
171
            }
172
        }
173
174
        return $cache;
175
    }
176
177
    /**
178
     * @param RequestInterface  $request
179
     * @param ResponseInterface $response
180
     *
181
     * @return bool true if success
182
     */
183
    public function cache(RequestInterface $request, ResponseInterface $response)
184
    {
185
        $reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control'));
186
        if ($reqCacheControl->has('no-store')) {
187
            // No caching allowed
188
            return false;
189
        }
190
191
        $cacheObject = $this->getCacheObject($request, $response);
192
        if ($cacheObject !== null) {
193
            return $this->storage->save(
194
                $this->getCacheKey($request),
195
                $cacheObject
196
            );
197
        }
198
199
        return false;
200
    }
201
202
    /**
203
     * @param RequestInterface $request
204
     * @param ResponseInterface $response
205
     *
206
     * @return bool true if success
207
     */
208
    public function update(RequestInterface $request, ResponseInterface $response)
209
    {
210
        return $this->cache($request, $response);
211
    }
212
}
213