Completed
Pull Request — master (#12)
by Graham
13:02 queued 08:50
created

CachePlugin::handleRequest()   C

Complexity

Conditions 7
Paths 3

Size

Total Lines 40
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 7.392

Importance

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