1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* This file is part of the PHPMongo package. |
5
|
|
|
* |
6
|
|
|
* (c) Dmytro Sokil <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Sokil\Mongo; |
13
|
|
|
|
14
|
|
|
use Psr\SimpleCache\CacheInterface; |
15
|
|
|
use Sokil\Mongo\Cache\Exception\CacheException; |
16
|
|
|
use Sokil\Mongo\Cache\Exception\InvalidArgumentException; |
17
|
|
|
|
18
|
|
|
class Cache implements \Countable, CacheInterface |
19
|
|
|
{ |
20
|
|
|
const FIELD_NAME_VALUE = 'v'; |
21
|
|
|
const FIELD_NAME_EXPIRED = 'e'; |
22
|
|
|
const FIELD_NAME_TAGS = 't'; |
23
|
|
|
|
24
|
|
|
private $collection; |
25
|
|
|
|
26
|
|
|
public function __construct(Database $database, $collectionName) |
27
|
|
|
{ |
28
|
|
|
$this->collection = $database |
29
|
|
|
->map($collectionName, array( |
30
|
|
|
'index' => array( |
31
|
|
|
// date field |
32
|
|
|
array( |
33
|
|
|
'keys' => array(self::FIELD_NAME_EXPIRED => 1), |
34
|
|
|
'expireAfterSeconds' => 0 |
35
|
|
|
), |
36
|
|
|
) |
37
|
|
|
)) |
38
|
|
|
->getCollection($collectionName) |
39
|
|
|
->disableDocumentPool(); |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* @return Cache |
44
|
|
|
*/ |
45
|
|
|
public function init() |
46
|
|
|
{ |
47
|
|
|
$this->collection->initIndexes(); |
48
|
|
|
return $this; |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* Persists a set of key => value pairs in the cache, with an optional TTL. |
53
|
|
|
* |
54
|
|
|
* @param array $values A list of key => value pairs for a multiple-set operation. |
55
|
|
|
* @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and |
56
|
|
|
* the driver supports TTL then the library may set a default value |
57
|
|
|
* for it or let the driver take care of that. |
58
|
|
|
* @param array $tags List of tags |
59
|
|
|
* |
60
|
|
|
* @return bool True on success and false on failure. |
61
|
|
|
* |
62
|
|
|
* @throws \Psr\SimpleCache\InvalidArgumentException |
63
|
|
|
* MUST be thrown if $values is neither an array nor a Traversable, |
64
|
|
|
* or if any of the $values are not a legal value. |
65
|
|
|
*/ |
66
|
|
|
public function setMultiple($values, $ttl = null, array $tags = array()) |
67
|
|
|
{ |
68
|
|
|
// prepare expiration |
69
|
|
|
if (!empty($ttl)) { |
70
|
|
View Code Duplication |
if ($ttl instanceof \DateInterval) { |
|
|
|
|
71
|
|
|
$ttl = $ttl->s; |
72
|
|
|
} elseif (!is_int($ttl)) { |
73
|
|
|
throw new InvalidArgumentException('Invalid TTL specified'); |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
$expirationTimestamp = time() + $ttl; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
// prepare persistence |
80
|
|
|
$persistence = $this->collection->getDatabase()->getClient()->createPersistence(); |
81
|
|
|
|
82
|
|
|
// prepare documents to store |
83
|
|
|
foreach ($values as $key => $value) { |
84
|
|
|
// create document |
85
|
|
|
$document = array( |
86
|
|
|
'_id' => $key, |
87
|
|
|
self::FIELD_NAME_VALUE => $value, |
88
|
|
|
); |
89
|
|
|
|
90
|
|
|
// add expiration |
91
|
|
|
if (!empty($expirationTimestamp)) { |
92
|
|
|
$document[self::FIELD_NAME_EXPIRED] = new \MongoDate($expirationTimestamp); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
// prepare tags |
96
|
|
|
if (!empty($tags)) { |
97
|
|
|
$document[self::FIELD_NAME_TAGS] = $tags; |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
// attach document |
101
|
|
|
$persistence->persist($this->collection->createDocument($document)); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
try { |
105
|
|
|
$persistence->flush(); |
106
|
|
|
} catch (\Exception $e) { |
107
|
|
|
return false; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
return true; |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Set with expiration on concrete date |
115
|
|
|
* |
116
|
|
|
* @deprecated Use self::set() with calculated ttl |
117
|
|
|
* |
118
|
|
|
* @param int|string $key |
119
|
|
|
* @param mixed $value |
120
|
|
|
* @param int $expirationTime |
121
|
|
|
* @param array $tags |
122
|
|
|
* |
123
|
|
|
* @throws Exception |
124
|
|
|
* |
125
|
|
|
* @return bool |
126
|
|
|
*/ |
127
|
|
|
public function setDueDate($key, $value, $expirationTime, array $tags = null) |
128
|
|
|
{ |
129
|
|
|
return $this->set($key, $value, $expirationTime - time(), $tags); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Set key that never expired |
134
|
|
|
* |
135
|
|
|
* @deprecated Use self::set() with null in ttl |
136
|
|
|
* |
137
|
|
|
* @param int|string $key |
138
|
|
|
* @param mixed $value |
139
|
|
|
* @param array $tags |
140
|
|
|
* |
141
|
|
|
* @return bool |
142
|
|
|
*/ |
143
|
|
|
public function setNeverExpired($key, $value, array $tags = null) |
144
|
|
|
{ |
145
|
|
|
return $this->set($key, $value, null, $tags); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. |
150
|
|
|
* |
151
|
|
|
* @param string $key The key of the item to store. |
152
|
|
|
* @param mixed $value The value of the item to store, must be serializable. |
153
|
|
|
* @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and |
154
|
|
|
* the driver supports TTL then the library may set a default value |
155
|
|
|
* for it or let the driver take care of that. |
156
|
|
|
* @param array $tags List of tags |
157
|
|
|
* |
158
|
|
|
* @return bool True on success and false on failure. |
159
|
|
|
* |
160
|
|
|
* @throws InvalidArgumentException |
161
|
|
|
* @throws CacheException |
162
|
|
|
*/ |
163
|
|
|
public function set($key, $value, $ttl = null, array $tags = null) |
164
|
|
|
{ |
165
|
|
|
// create document |
166
|
|
|
$document = array( |
167
|
|
|
'_id' => $key, |
168
|
|
|
self::FIELD_NAME_VALUE => $value, |
169
|
|
|
); |
170
|
|
|
|
171
|
|
|
// prepare expiration |
172
|
|
|
if (!empty($ttl)) { |
173
|
|
View Code Duplication |
if ($ttl instanceof \DateInterval) { |
|
|
|
|
174
|
|
|
$ttl = $ttl->s; |
175
|
|
|
} elseif (!is_int($ttl)) { |
176
|
|
|
throw new InvalidArgumentException('Invalid TTL specified'); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
$expirationTimestamp = time() + $ttl; |
180
|
|
|
$document[self::FIELD_NAME_EXPIRED] = new \MongoDate((int) $expirationTimestamp); |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
// prepare tags |
184
|
|
|
if (!empty($tags)) { |
185
|
|
|
$document[self::FIELD_NAME_TAGS] = $tags; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
// create document |
189
|
|
|
$result = $this |
190
|
|
|
->collection |
191
|
|
|
->getMongoCollection() |
192
|
|
|
->update( |
193
|
|
|
array( |
194
|
|
|
'_id' => $key, |
195
|
|
|
), |
196
|
|
|
$document, |
197
|
|
|
array( |
198
|
|
|
'upsert' => true, |
199
|
|
|
) |
200
|
|
|
); |
201
|
|
|
|
202
|
|
|
// check result |
203
|
|
|
return (double) 1 === $result['ok']; |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* @param array $keys |
208
|
|
|
* @param mixed|null $default |
209
|
|
|
* |
210
|
|
|
* @return array |
211
|
|
|
*/ |
212
|
|
|
public function getMultiple($keys, $default = null) |
213
|
|
|
{ |
214
|
|
|
// Prepare defaults |
215
|
|
|
$values = array_fill_keys($keys, $default); |
216
|
|
|
|
217
|
|
|
// Get document |
218
|
|
|
$documents = $this->collection->getDocuments($keys); |
219
|
|
|
if (empty($documents)) { |
220
|
|
|
return $values; |
|
|
|
|
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
// Mongo deletes document not exactly when set in field |
224
|
|
|
// Required date checking |
225
|
|
|
// Expiration may be empty for keys which never expired |
226
|
|
|
foreach ($documents as $document) { |
227
|
|
|
/** @var \MongoDate $expiredAt */ |
228
|
|
|
$expiredAt = $document->get(self::FIELD_NAME_EXPIRED); |
229
|
|
|
if (empty($expiredAt) || $expiredAt->sec >= time()) { |
230
|
|
|
$values[$document->getId()] = $document->get(self::FIELD_NAME_VALUE); |
231
|
|
|
} else { |
232
|
|
|
$values[$document->getId()] = $default; |
233
|
|
|
} |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
return $values; |
|
|
|
|
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* Get value by key |
241
|
|
|
* |
242
|
|
|
* @param string $key |
243
|
|
|
* @param mixed $default |
244
|
|
|
* |
245
|
|
|
* @return array|null |
246
|
|
|
*/ |
247
|
|
|
public function get($key, $default = null) |
248
|
|
|
{ |
249
|
|
|
// Get document |
250
|
|
|
$document = $this->collection->getDocument($key); |
251
|
|
|
if (!$document) { |
252
|
|
|
return $default; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
// Mongo deletes document not exactly when set in field |
256
|
|
|
// Required date checking |
257
|
|
|
// Expiration may be empty for keys which never expired |
258
|
|
|
$expiredAt = $document->get(self::FIELD_NAME_EXPIRED); |
259
|
|
|
if (!empty($expiredAt) && $expiredAt->sec < time()) { |
260
|
|
|
return $default; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
// Return value |
264
|
|
|
return $document->get(self::FIELD_NAME_VALUE); |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* @return Cache |
270
|
|
|
*/ |
271
|
|
|
public function clear() |
272
|
|
|
{ |
273
|
|
|
$this->collection->delete(); |
274
|
|
|
return $this; |
|
|
|
|
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Delete an item from the cache by its unique key. |
279
|
|
|
* |
280
|
|
|
* @param string $key The unique cache key of the item to delete. |
281
|
|
|
* |
282
|
|
|
* @return bool True if the item was successfully removed. False if there was an error. |
283
|
|
|
* |
284
|
|
|
* @throws \Psr\SimpleCache\InvalidArgumentException |
285
|
|
|
* MUST be thrown if the $key string is not a legal value. |
286
|
|
|
*/ |
287
|
|
|
public function delete($key) |
288
|
|
|
{ |
289
|
|
|
if (empty($key) || !is_string($key)) { |
290
|
|
|
throw new InvalidArgumentException('Key must be string'); |
291
|
|
|
} |
292
|
|
|
try { |
293
|
|
|
$this->collection->batchDelete(array( |
294
|
|
|
'_id' => $key, |
295
|
|
|
)); |
296
|
|
|
} catch (\Exception $e) { |
297
|
|
|
return false; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
return true; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* Deletes multiple cache items in a single operation. |
305
|
|
|
* |
306
|
|
|
* @param array $keys A list of string-based keys to be deleted. |
307
|
|
|
* |
308
|
|
|
* @return bool True if the items were successfully removed. False if there was an error. |
309
|
|
|
* |
310
|
|
|
* @throws \Psr\SimpleCache\InvalidArgumentException |
311
|
|
|
* MUST be thrown if $keys is neither an array nor a Traversable, |
312
|
|
|
* or if any of the $keys are not a legal value. |
313
|
|
|
*/ |
314
|
|
|
public function deleteMultiple($keys) |
315
|
|
|
{ |
316
|
|
|
try { |
317
|
|
|
$this->collection->batchDelete( |
318
|
|
|
function(Expression $e) use($keys) { |
319
|
|
|
$e->whereIn('_id', $keys); |
320
|
|
|
} |
321
|
|
|
); |
322
|
|
|
} catch (\Exception $e) { |
323
|
|
|
return false; |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
return true; |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
/** |
330
|
|
|
* Delete documents by tag |
331
|
|
|
*/ |
332
|
|
|
public function deleteMatchingTag($tag) |
333
|
|
|
{ |
334
|
|
|
$this->collection->batchDelete(function (\Sokil\Mongo\Expression $e) use ($tag) { |
335
|
|
|
return $e->where(Cache::FIELD_NAME_TAGS, $tag); |
336
|
|
|
}); |
337
|
|
|
|
338
|
|
|
return $this; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
/** |
342
|
|
|
* Delete documents by tag |
343
|
|
|
*/ |
344
|
|
|
public function deleteNotMatchingTag($tag) |
345
|
|
|
{ |
346
|
|
|
$this->collection->batchDelete(function (\Sokil\Mongo\Expression $e) use ($tag) { |
347
|
|
|
return $e->whereNotEqual(Cache::FIELD_NAME_TAGS, $tag); |
348
|
|
|
}); |
349
|
|
|
|
350
|
|
|
return $this; |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
/** |
354
|
|
|
* Delete documents by tag |
355
|
|
|
* Document deletes if it contains all passed tags |
356
|
|
|
*/ |
357
|
|
|
public function deleteMatchingAllTags(array $tags) |
358
|
|
|
{ |
359
|
|
|
$this->collection->batchDelete(function (\Sokil\Mongo\Expression $e) use ($tags) { |
360
|
|
|
return $e->whereAll(Cache::FIELD_NAME_TAGS, $tags); |
361
|
|
|
}); |
362
|
|
|
|
363
|
|
|
return $this; |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
/** |
367
|
|
|
* Delete documents by tag |
368
|
|
|
* Document deletes if it not contains all passed tags |
369
|
|
|
*/ |
370
|
|
|
public function deleteMatchingNoneOfTags(array $tags) |
371
|
|
|
{ |
372
|
|
|
$this->collection->batchDelete(function (\Sokil\Mongo\Expression $e) use ($tags) { |
373
|
|
|
return $e->whereNoneOf(Cache::FIELD_NAME_TAGS, $tags); |
374
|
|
|
}); |
375
|
|
|
|
376
|
|
|
return $this; |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
/** |
380
|
|
|
* Delete documents by tag |
381
|
|
|
* Document deletes if it contains any of passed tags |
382
|
|
|
*/ |
383
|
|
|
public function deleteMatchingAnyTag(array $tags) |
384
|
|
|
{ |
385
|
|
|
$this->collection->batchDelete(function (\Sokil\Mongo\Expression $e) use ($tags) { |
386
|
|
|
return $e->whereIn(Cache::FIELD_NAME_TAGS, $tags); |
387
|
|
|
}); |
388
|
|
|
|
389
|
|
|
return $this; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* Delete documents by tag |
394
|
|
|
* Document deletes if it contains any of passed tags |
395
|
|
|
*/ |
396
|
|
|
public function deleteNotMatchingAnyTag(array $tags) |
397
|
|
|
{ |
398
|
|
|
$this->collection->batchDelete(function (\Sokil\Mongo\Expression $e) use ($tags) { |
399
|
|
|
return $e->whereNotIn(Cache::FIELD_NAME_TAGS, $tags); |
400
|
|
|
}); |
401
|
|
|
|
402
|
|
|
return $this; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* Get total count of documents in cache |
407
|
|
|
* |
408
|
|
|
* @return int |
409
|
|
|
*/ |
410
|
|
|
public function count() |
411
|
|
|
{ |
412
|
|
|
return $this->collection->count(); |
413
|
|
|
} |
414
|
|
|
|
415
|
|
|
public function has($key) |
416
|
|
|
{ |
417
|
|
|
return (bool) $this->get($key); |
418
|
|
|
} |
419
|
|
|
} |
420
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.