Completed
Pull Request — master (#21)
by Tobias
14:56 queued 12:50
created

CachePlugin::setConfig()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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

This check looks for function calls that miss required arguments.

Loading history...
Documentation introduced by
array('default_ttl' => a...ders' => array('bool')) is of type array<string,array<integ...,{\"0\":\"string\"}>"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
203 6
                'default_ttl' => ['int', 'null'],
204 6
                'respect_cache_headers' => ['bool'],
205 6
            ]);
206 6
        } else {
207
            $resolver->setAllowedTypes('default_ttl', ['int', 'null']);
208
            $resolver->setAllowedTypes('respect_cache_headers', 'bool');
209
        }
210 6
    }
211
212
    /**
213
     * Set config to the plugin. This will overwrite any previously set config values.
214
     *
215
     * @param array $config
216
     */
217 6
    public function setConfig(array $config)
218
    {
219 6
        $this->config = $this->optionsResolver->resolve($config);
220 6
    }
221
}
222