Completed
Pull Request — master (#19)
by Tobias
02:23
created

CachePlugin::__construct()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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