Failed Conditions
Push — v3.x ( babbd8...d960e5 )
by Chad
02:14
created

src/Cache/MongoCache.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Chadicus\Marvel\Api\Cache;
4
5
use DominionEnterprises\Util\Arrays;
6
use GuzzleHttp\Psr7\Response;
7
use MongoDB\BSON\UTCDateTime;
8
use Psr\Http\Message\ResponseInterface;
9
use Psr\SimpleCache\CacheInterface;
10
11
/**
12
 * A PSR-16 implementation which stores data in an array.
13
 */
14
final class MongoCache extends AbstractCache implements CacheInterface
15
{
16
    /**
17
     * MongoDB collection containing the cached responses.
18
     *
19
     * @var Collection
20
     */
21
    private $collection;
22
23
    /**
24
     * Construct a new instance of MongoCache.
25
     *
26
     * @param Collection $collection        The collection containing the cached data.
27
     * @param integer    $defaultTimeToLive The default time to live in seconds.
28
     */
29
    public function __construct(Collection $collection)
30
    {
31
        $this->collection = $collection;
32
        $this->collection->createIndex(['expires' => 1], ['expireAfterSeconds' => 0, 'background' => true]);
33
    }
34
35
    /**
36
     * Fetches a value from the cache.
37
     *
38
     * @param string $key     The unique key of this item in the cache.
39
     * @param mixed  $default Default value to return if the key does not exist.
40
     *
41
     * @return mixed The value of the item from the cache, or $default in case of cache miss.
42
     *
43
     * @throws InvalidArgumentException Thrown if the $key string is not a legal value.
44
     */
45
    public function get($key, $default = null)
46
    {
47
        $this->verifyKey($key);
48
        $cached = $this->collection->findOne(
49
            ['_id' => $key],
50
            ['typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array']]
51
        );
52
53
        if ($cached === null) {
54
            return $default;
55
        }
56
57
        return new Response(
58
            $cached['statusCode'],
59
            $cached['headers'],
60
            $cached['body'],
61
            $cached['protocolVersion'],
62
            $cached['reasonPhrase']
63
        );
64
    }
65
66
    /**
67
     * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time.
68
     *
69
     * @param string                $key   The key of the item to store.
70
     * @param mixed                 $value The value of the item to store, must be serializable.
71
     * @param null|int|DateInterval $ttl   Optional. The TTL value of this item. If no value is sent and
72
     *                                     the driver supports TTL then the library may set a default value
73
     *                                     for it or let the driver take care of that.
74
     *
75
     * @return bool True on success and false on failure.
76
     *
77
     * @throws InvalidArgumentException Thrown if the $key string is not a legal value.
78
     */
79
    public function set($key, $value, $ttl = null)
80
    {
81
        $this->verifyKey($key);
82
        $this->verifyValue($value);
83
        return $this->updateCache($key, $value, $this->getExpires($ttl));
84
    }
85
86
    /**
87
     * Delete an item from the cache by its unique key.
88
     *
89
     * @param string $key The unique cache key of the item to delete.
90
     *
91
     * @return bool True if the item was successfully removed. False if there was an error.
92
     *
93
     * @throws InvalidArgumentException Thrown if the $key string is not a legal value.
94
     */
95
    public function delete($key)
96
    {
97
        $this->verifyKey($key);
98
        try {
99
            $this->collection->deleteOne(['_id' => $key]);
100
            return true;
101
        } catch (\Exception $e) {
102
            return false;
103
        }
104
    }
105
106
    /**
107
     * Wipes clean the entire cache's keys.
108
     *
109
     * @return bool True on success and false on failure.
110
     */
111
    public function clear()
112
    {
113
        try {
114
            $this->collection->deleteMany([]);
115
            return true;
116
        } catch (\Exception $e) {
117
            return false;
118
        }
119
    }
120
121
    /**
122
     * Obtains multiple cache items by their unique keys.
123
     *
124
     * @param iterable $keys    A list of keys that can obtained in a single operation.
125
     * @param mixed    $default Default value to return for keys that do not exist.
126
     *
127
     * @return iterable A list of key => value pairs. Cache keys that do not exist or are stale will have $default as value.
128
     *
129
     * @throws InvalidArgumentException Thrown if the $key string is not a legal value.
130
     */
131 View Code Duplication
    public function getMultiple($keys, $default = null)
132
    {
133
        $items = [];
134
        foreach ($keys as $key) {
135
            $items[$key] = $this->get($key, $default);
136
        }
137
138
        return $items;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $items; (array) is incompatible with the return type declared by the interface Psr\SimpleCache\CacheInterface::getMultiple of type Psr\SimpleCache\iterable.

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...
139
    }
140
141
    /**
142
     * Persists a set of key => value pairs in the cache, with an optional TTL.
143
     *
144
     * @param iterable              $values A list of key => value pairs for a multiple-set operation.
145
     * @param null|int|DateInterval $ttl    Optional. The TTL value of this item. If no value is sent and
146
     *                                      the driver supports TTL then the library may set a default value
147
     *                                      for it or let the driver take care of that.
148
     *
149
     * @return bool True on success and false on failure.
150
     *
151
     * @throws InvalidArgumentException Thrown if $values is neither an array nor a Traversable,
152
     *                                  or if any of the $values are not a legal value.
153
     */
154
    public function setMultiple($values, $ttl = null)
155
    {
156
        foreach ($values as $key => $value) {
157
            $this->verfiyKey($key);
0 ignored issues
show
The method verfiyKey() does not seem to exist on object<Chadicus\Marvel\Api\Cache\MongoCache>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
158
            $this->verifyValue($value);
159
            $expires = $this->getExpires($ttl);
160
            try {
161
                $this->updateCache($key, $value, $expires);
162
            } catch (\Exception $e) {
163
                return false;
164
            }
165
        }
166
167
        return true;
168
    }
169
170
    /**
171
     * Deletes multiple cache items in a single operation.
172
     *
173
     * @param iterable $keys A list of string-based keys to be deleted.
174
     *
175
     * @return bool True if the items were successfully removed. False if there was an error.
176
     *
177
     * @throws InvalidArgumentException Thrown if $keys is neither an array nor a Traversable,
178
     *                                  or if any of the $keys are not a legal value.
179
     */
180
    public function deleteMultiple($keys)
181
    {
182
        array_walk($keys, [$this, 'verifyKey']);
183
184
        try {
185
            $deleteManyResult = $this->deleteMany(['_id' => ['$in' => $keys]]);
0 ignored issues
show
The method deleteMany() does not exist on Chadicus\Marvel\Api\Cache\MongoCache. Did you maybe mean delete()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
186
            return $deleteManyResult->getDeletedCount() === count($keys);
187
        } catch (\Exception $e) {
188
            return false;
189
        }
190
    }
191
192
    /**
193
     * Determines whether an item is present in the cache.
194
     *
195
     * NOTE: It is recommended that has() is only to be used for cache warming type purposes
196
     * and not to be used within your live applications operations for get/set, as this method
197
     * is subject to a race condition where your has() will return true and immediately after,
198
     * another script can remove it making the state of your app out of date.
199
     *
200
     * @param string $key The cache item key.
201
     *
202
     * @return bool
203
     *
204
     * @throws InvalidArgumentException Thrown if the $key string is not a legal value.
205
     */
206
    public function has($key)
207
    {
208
        $this->verifyKey($key);
209
        return $this->collection->count(['_id' => $key]) === 1;
210
    }
211
212
    /**
213
     * Upserts a PSR-7 response in the cache.
214
     *
215
     * @param string $key The key of the response to store.
216
     * @param ResponseInterface $response The response to store.
217
     * @param UTCDateTime $expires The expire date of the cache item.
218
     *
219
     * @return bool
220
     */
221
    private function updateCache(string $key, ResponseInterface $response, UTCDateTime $expires) : bool
222
    {
223
        $document = [
224
            'statusCode' => $response->getStatusCode(),
225
            'headers' => $response->getHeaders(),
226
            'body' => (string)$response->getBody(),
227
            'protocolVersion' => $response->getProtocolVersion(),
228
            'reasonPhrase' => $response->getReasonPhrase(),
229
            'expires' => $expires,
230
        ];
231
232
        try {
233
            $this->collection->updateOne(['_id' => $id], ['$set' => $document], ['upsert' => true]);
0 ignored issues
show
The variable $id does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
234
            return true;
235
        } catch (\Exception $e) {
236
            return false;
237
        }
238
    }
239
240
    /**
241
     * Converts the given time to live value to a UTCDateTime instance;
242
     *
243
     * @param mixed $key The cache key to validate.
244
     *
245
     * @return UTCDateTime
246
     *
247
     * @throws InvalidArgumentException Thrown if the $ttl is not null, an integer or \DateInterval.
248
     */
249
    private function getExpires($ttl)
250
    {
251
        if ($ttl === null) {
252
            $ttl = 86400;
253
        }
254
255
        if ($ttl instanceof \DateInterval) {
256
            $ttl = $ttl->s;
257
        }
258
259
        if (is_int($ttl)) {
260
            return new UTCDateTime((time() + $ttl) * 1000);
261
        }
262
263
        throw new InvalidArgumentException('$ttl must be null, an integer or \DateInterval instance');
264
    }
265
}
266