Completed
Push — master ( 37fa4e...c6f75f )
by Joel
04:45
created

CachePlugin::configureOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

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