Completed
Pull Request — master (#21)
by Tobias
37:49 queued 27:51
created

CachePlugin::configure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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