Completed
Push — master ( 3557c6...b165dc )
by Márk
10:18 queued 06:53
created

CachePlugin::isCacheable()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4.0741

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 11
ccs 5
cts 6
cp 0.8333
rs 9.2
cc 4
eloc 6
nc 3
nop 1
crap 4.0741
1
<?php
2
3
namespace Http\Client\Plugin;
4
5
use Http\Client\Tools\Promise\FulfilledPromise;
6
use Psr\Cache\CacheItemPoolInterface;
7
use Psr\Http\Message\RequestInterface;
8
use Psr\Http\Message\ResponseInterface;
9
10
/**
11
 * Allow for caching a response.
12
 *
13
 * @author Tobias Nyholm <[email protected]>
14
 */
15
class CachePlugin implements Plugin
16
{
17
    /**
18
     * @var CacheItemPoolInterface
19
     */
20
    private $pool;
21
22
    /**
23
     * Default time to store object in cache. This value is used if CachePlugin::respectCacheHeaders is false or
24
     * if cache headers are missing.
25
     *
26
     * @var int
27
     */
28
    private $defaultTtl;
29
30
    /**
31
     * Look at the cache headers to know how long this response is going to be cached.
32
     *
33
     * @var bool
34
     */
35
    private $respectCacheHeaders;
36
37
    /**
38
     * @param CacheItemPoolInterface $pool
39
     * @param array                  $options
40
     */
41 6
    public function __construct(CacheItemPoolInterface $pool, array $options = [])
42
    {
43 6
        $this->pool = $pool;
44 6
        $this->defaultTtl = isset($options['default_ttl']) ? $options['default_ttl'] : null;
45 6
        $this->respectCacheHeaders = isset($options['respect_cache_headers']) ? $options['respect_cache_headers'] : true;
46 6
    }
47
48
    /**
49
     * {@inheritdoc}
50
     */
51 4
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
52
    {
53 4
        $method = strtoupper($request->getMethod());
54
55
        // if the request not is cachable, move to $next
56 4
        if ($method !== 'GET' && $method !== 'HEAD') {
57 1
            return $next($request);
58
        }
59
60
        // If we can cache the request
61 3
        $key = $this->createCacheKey($request);
62 3
        $cacheItem = $this->pool->getItem($key);
63
64 3
        if ($cacheItem->isHit()) {
65
            // return cached response
66
            return new FulfilledPromise($cacheItem->get());
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \Http\Client\...ise($cacheItem->get()); (Http\Client\Tools\Promise\FulfilledPromise) is incompatible with the return type declared by the interface Http\Client\Plugin\Plugin::handleRequest of type Http\Client\Promise.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
67
        }
68
69 3
        return $next($request)->then(function (ResponseInterface $response) use ($cacheItem) {
70 3
            if ($this->isCacheable($response)) {
71 2
                $cacheItem->set($response)
72 2
                    ->expiresAfter($this->getMaxAge($response));
73 2
                $this->pool->save($cacheItem);
74 2
            }
75
76 3
            return $response;
77 3
        });
78
    }
79
80
    /**
81
     * Verify that we can cache this response.
82
     *
83
     * @param ResponseInterface $response
84
     *
85
     * @return bool
86
     */
87 3
    protected function isCacheable(ResponseInterface $response)
88
    {
89 3
        if (!in_array($response->getStatusCode(), [200, 203, 300, 301, 302, 404, 410])) {
90 1
            return false;
91
        }
92 2
        if ($this->getCacheControlDirective($response, 'no-store') || $this->getCacheControlDirective($response, 'private')) {
93
            return false;
94
        }
95
96 2
        return true;
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
     *
106
     * @return bool|string
107
     */
108 2
    private function getCacheControlDirective(ResponseInterface $response, $name)
109
    {
110 2
        $headers = $response->getHeader('Cache-Control');
111 2
        foreach ($headers as $header) {
112 1
            if (preg_match(sprintf('|%s=?([0-9]+)?|i', $name), $header, $matches)) {
113
114
                // return the value for $name if it exists
115 1
                if (isset($matches[1])) {
116 1
                    return $matches[1];
117
                }
118
119
                return true;
120
            }
121 2
        }
122
123 2
        return false;
124
    }
125
126
    /**
127
     * @param RequestInterface $request
128
     *
129
     * @return string
130
     */
131 3
    private function createCacheKey(RequestInterface $request)
132
    {
133 3
        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
     *
141
     * @return int|null
142
     */
143 2
    private function getMaxAge(ResponseInterface $response)
144
    {
145 2
        if (!$this->respectCacheHeaders) {
146
            return $this->defaultTtl;
147
        }
148
149
        // check for max age in the Cache-Control header
150 2
        $maxAge = $this->getCacheControlDirective($response, 'max-age');
151 2
        if (!is_bool($maxAge)) {
152 1
            $ageHeaders = $response->getHeader('Age');
153 1
            foreach ($ageHeaders as $age) {
154 1
                return $maxAge - ((int) $age);
155
            }
156
157
            return $maxAge;
158
        }
159
160
        // check for ttl in the Expires header
161 1
        $headers = $response->getHeader('Expires');
162 1
        foreach ($headers as $header) {
163
            return (new \DateTime($header))->getTimestamp() - (new \DateTime())->getTimestamp();
164 1
        }
165
166 1
        return $this->defaultTtl;
167
    }
168
}
169