Completed
Pull Request — master (#21)
by Tobias
03:09 queued 01:01
created

CachePlugin::isCacheable()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4.0741

Importance

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