Completed
Pull Request — master (#21)
by Tobias
19:42 queued 09:45
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->respectCacheHeaders && $privateHeaders);
0 ignored issues
show
Bug introduced by
The property respectCacheHeaders does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
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