Completed
Push — master ( 09694c...01ea43 )
by Márk
02:34
created

CachePlugin::getMaxAge()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5.4743

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 25
ccs 11
cts 15
cp 0.7333
rs 8.439
cc 5
eloc 13
nc 5
nop 1
crap 5.4743
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
        $cachableCodes = [200, 203, 300, 301, 302, 404, 410];
90 3
        $privateHeaders = $this->getCacheControlDirective($response, 'no-store') || $this->getCacheControlDirective($response, 'private');
91
92
        // If http status code is cachable and if we respect the headers, make sure there is no private cache headers.
93 3
        return in_array($response->getStatusCode(), $cachableCodes) && !($this->respectCacheHeaders && $privateHeaders);
94
    }
95
96
    /**
97
     * Returns the value of a parameter in the cache control header. If not found we return false. If found with no
98
     * value return true.
99
     *
100
     * @param ResponseInterface $response
101
     * @param string            $name
102
     *
103
     * @return bool|string
104
     */
105 3
    private function getCacheControlDirective(ResponseInterface $response, $name)
106
    {
107 3
        $headers = $response->getHeader('Cache-Control');
108 3
        foreach ($headers as $header) {
109 1
            if (preg_match(sprintf('|%s=?([0-9]+)?|i', $name), $header, $matches)) {
110
111
                // return the value for $name if it exists
112 1
                if (isset($matches[1])) {
113 1
                    return $matches[1];
114
                }
115
116
                return true;
117
            }
118 3
        }
119
120 3
        return false;
121
    }
122
123
    /**
124
     * @param RequestInterface $request
125
     *
126
     * @return string
127
     */
128 3
    private function createCacheKey(RequestInterface $request)
129
    {
130 3
        return md5($request->getMethod().' '.$request->getUri());
131
    }
132
133
    /**
134
     * Get a ttl in seconds. It could return null if we do not respect cache headers and got no defaultTtl.
135
     *
136
     * @param ResponseInterface $response
137
     *
138
     * @return int|null
139
     */
140 2
    private function getMaxAge(ResponseInterface $response)
141
    {
142 2
        if (!$this->respectCacheHeaders) {
143
            return $this->defaultTtl;
144
        }
145
146
        // check for max age in the Cache-Control header
147 2
        $maxAge = $this->getCacheControlDirective($response, 'max-age');
148 2
        if (!is_bool($maxAge)) {
149 1
            $ageHeaders = $response->getHeader('Age');
150 1
            foreach ($ageHeaders as $age) {
151 1
                return $maxAge - ((int) $age);
152
            }
153
154
            return $maxAge;
155
        }
156
157
        // check for ttl in the Expires header
158 1
        $headers = $response->getHeader('Expires');
159 1
        foreach ($headers as $header) {
160
            return (new \DateTime($header))->getTimestamp() - (new \DateTime())->getTimestamp();
161 1
        }
162
163 1
        return $this->defaultTtl;
164
    }
165
}
166