1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* |
4
|
|
|
* This file is part of the Apix Project. |
5
|
|
|
* |
6
|
|
|
* (c) Franck Cassedanne <franck at ouarz.net> |
7
|
|
|
* |
8
|
|
|
* @license http://opensource.org/licenses/BSD-3-Clause New BSD License |
9
|
|
|
* |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Apix\Cache; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* Memcached cache wrapper. |
16
|
|
|
* |
17
|
|
|
* @see http://code.google.com/p/memcached/wiki/NewProgrammingTricks |
18
|
|
|
* @see http://dustin.github.com/2011/02/17/memcached-set.html |
19
|
|
|
* |
20
|
|
|
* @package Apix\Cache |
21
|
|
|
* @author Franck Cassedanne <franck at ouarz.net> |
22
|
|
|
*/ |
23
|
|
|
class Memcached extends AbstractCache |
24
|
|
|
{ |
25
|
|
|
/** |
26
|
|
|
* Holds an injected adapter. |
27
|
|
|
* @var \Memcached |
28
|
|
|
*/ |
29
|
|
|
protected $adapter = null; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Holds the array of TTLs. |
33
|
|
|
* @var array |
34
|
|
|
*/ |
35
|
|
|
protected $ttls = array(); |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Constructor. |
39
|
|
|
* |
40
|
|
|
* @param \Memcached $memcached A Memcached instance. |
41
|
|
|
* @param array $options Array of options. |
42
|
|
|
*/ |
43
|
66 |
|
public function __construct(\Memcached $memcached, array $options = null) |
44
|
|
|
{ |
45
|
|
|
// default options |
46
|
66 |
|
$this->options['prefix_key'] = 'key_'; // prefix cache keys |
47
|
66 |
|
$this->options['prefix_tag'] = 'tag_'; // prefix cache tags |
48
|
66 |
|
$this->options['prefix_idx'] = 'idx_'; // prefix cache indexes |
49
|
66 |
|
$this->options['prefix_nsp'] = 'nsp_'; // prefix cache namespaces |
50
|
|
|
|
51
|
66 |
|
// 'auto' is igbinary or msgpack if available, php otherwise. |
52
|
|
|
$this->options['serializer'] = 'auto'; // auto, php, json, json_array |
53
|
66 |
|
// igBinary and msgpack |
54
|
|
|
|
55
|
66 |
|
parent::__construct($memcached, $options); |
56
|
|
|
|
57
|
66 |
|
$memcached->setOption(\Memcached::OPT_COMPRESSION, false); |
58
|
66 |
|
|
59
|
66 |
|
if ($this->options['tag_enable']) { |
60
|
66 |
|
$memcached->setOption(\Memcached::OPT_BINARY_PROTOCOL, false); |
61
|
66 |
|
$this->setSerializer($this->options['serializer']); |
62
|
66 |
|
$this->setNamespace($this->options['prefix_nsp']); |
63
|
|
|
} |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
18 |
|
* {@inheritdoc} |
68
|
|
|
*/ |
69
|
18 |
|
public function loadKey($key) |
70
|
|
|
{ |
71
|
|
|
return $this->get($this->mapKey($key)); |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
22 |
|
* {@inheritdoc} |
76
|
|
|
*/ |
77
|
22 |
|
public function loadTag($tag) |
78
|
|
|
{ |
79
|
|
|
return $this->getIndex($this->mapTag($tag))->load(); |
|
|
|
|
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
/** |
83
|
36 |
|
* {@inheritdoc} |
84
|
|
|
*/ |
85
|
36 |
|
public function save($data, $key, array $tags = null, $ttl = null) |
86
|
|
|
{ |
87
|
36 |
|
$ttl = $this->sanitiseTtl($ttl); |
88
|
|
|
|
89
|
36 |
|
$mKey = $this->mapKey($key); |
90
|
36 |
|
|
91
|
|
|
$data = array('data' => $data, 'ttl' => $ttl); |
92
|
|
|
$this->ttls[$mKey] = $ttl; |
93
|
36 |
|
|
94
|
|
|
// add the item |
95
|
36 |
|
$success = $this->adapter->set($mKey, $data, $ttl); |
96
|
|
|
|
97
|
|
|
if ($success && $this->options['tag_enable'] && !empty($tags)) { |
98
|
20 |
|
|
99
|
|
|
// add all the tags to the index key. |
100
|
|
|
$this->getIndex($this->mapIdx($key))->add($tags); |
101
|
20 |
|
|
102
|
20 |
|
// append the key to each tag. |
103
|
20 |
|
foreach ($tags as $tag) { |
104
|
20 |
|
$this->getIndex($this->mapTag($tag))->add($mKey); |
105
|
|
|
} |
106
|
36 |
|
} |
107
|
|
|
|
108
|
|
|
return $success; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Alias to `Memcached::deleteMulti` or loop `Memcached::delete`. |
113
|
|
|
* |
114
|
|
|
* @param array $items The items to be deleted. |
115
|
|
|
* |
116
|
8 |
|
* @return bool Returns TRUE on success or FALSE on failure. |
117
|
|
|
*/ |
118
|
8 |
|
protected function deleteMulti($items) |
119
|
8 |
|
{ |
120
|
|
|
if (method_exists($this->adapter, 'deleteMulti')) { |
121
|
8 |
|
$this->adapter->deleteMulti($items); |
122
|
|
|
|
123
|
|
|
return (boolean) $this->adapter->getResultCode() != \Memcached::RES_FAILURE; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
// Fix environments (some HHVM versions) that don't handle deleteMulti. |
127
|
|
|
// @see https://github.com/facebook/hhvm/issues/4602 |
128
|
|
|
// @codeCoverageIgnoreStart |
129
|
|
|
$success = true; |
130
|
|
|
foreach ($items as $item) { |
131
|
|
|
$success = $this->adapter->delete($item) && $success; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
return $success; |
135
|
|
|
// @codeCoverageIgnoreEnd |
136
|
|
|
} |
137
|
2 |
|
|
138
|
|
|
/** |
139
|
2 |
|
* {@inheritdoc} |
140
|
2 |
|
*/ |
141
|
2 |
|
public function clean(array $tags) |
142
|
2 |
|
{ |
143
|
2 |
|
$items = array(); |
144
|
2 |
|
foreach ($tags as $tag) { |
145
|
|
|
$keys = $this->loadTag($tag); |
146
|
2 |
|
if (null !== $keys) { |
147
|
2 |
|
foreach ($keys as $key) { |
148
|
|
|
$items[] = $key; |
149
|
2 |
|
// $items[] = $this->mapIdx($key); |
|
|
|
|
150
|
|
|
} |
151
|
|
|
} |
152
|
|
|
// add the tag to deletion |
153
|
2 |
|
$items[] = $this->mapTag($tag); |
154
|
|
|
|
155
|
2 |
|
// add the index key for deletion |
156
|
|
|
// $items[] = $this->mapTag($tag); |
|
|
|
|
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
return $this->deleteMulti($items); |
160
|
|
|
} |
161
|
6 |
|
|
162
|
|
|
/** |
163
|
6 |
|
* {@inheritdoc} |
164
|
6 |
|
*/ |
165
|
|
|
public function delete($key) |
166
|
6 |
|
{ |
167
|
4 |
|
$_key = $this->mapKey($key); |
168
|
|
|
$items = array($_key); |
169
|
|
|
|
170
|
4 |
|
if ($this->options['tag_enable']) { |
171
|
|
|
$idx_key = $this->mapIdx($key); |
172
|
4 |
|
|
173
|
|
|
// load the tags from the index key |
174
|
2 |
|
$tags = $this->getIndex($idx_key)->load(); |
175
|
2 |
|
|
176
|
2 |
|
if (is_array($tags)) { |
177
|
|
|
// mark the key as deleted in the tags. |
178
|
2 |
|
foreach ($tags as $tag) { |
179
|
2 |
|
$this->getIndex($this->mapTag($tag))->remove($_key); |
180
|
4 |
|
} |
181
|
|
|
// delete that index key |
182
|
6 |
|
$items[] = $idx_key; |
183
|
|
|
} |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
return $this->deleteMulti($items); |
187
|
|
|
} |
188
|
66 |
|
|
189
|
|
|
/** |
190
|
66 |
|
* {@inheritdoc} |
191
|
66 |
|
*/ |
192
|
|
|
public function flush($all = false) |
193
|
4 |
|
{ |
194
|
|
|
if (true === $all) { |
195
|
|
|
return $this->adapter->flush(); |
196
|
4 |
|
} |
197
|
|
|
$nsKey = $this->options['prefix_nsp']; |
198
|
4 |
|
|
199
|
|
|
// set a new namespace |
200
|
|
|
$success = $this->setNamespace($nsKey, true); |
201
|
|
|
|
202
|
|
|
return (boolean) $success; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
66 |
|
* {@inheritdoc} |
207
|
|
|
* |
208
|
|
|
* @param string $serializer |
209
|
|
|
*/ |
210
|
|
|
public function setSerializer($serializer) |
211
|
|
|
{ |
212
|
|
|
switch ($serializer) { |
213
|
|
|
|
214
|
|
|
case 'php': |
215
|
|
|
$opt = \Memcached::SERIALIZER_PHP; |
216
|
|
|
break; |
217
|
|
|
|
218
|
|
|
// @codeCoverageIgnoreStart |
219
|
|
|
case 'igBinary': |
220
|
|
|
if (!\Memcached::HAVE_IGBINARY) { |
221
|
|
|
continue; |
222
|
|
|
} |
223
|
|
|
$opt = \Memcached::SERIALIZER_IGBINARY; |
224
|
|
|
break; |
225
|
|
|
|
226
|
|
|
case 'json': |
227
|
|
|
if (!\Memcached::HAVE_JSON) { |
228
|
66 |
|
continue; |
229
|
66 |
|
} |
230
|
66 |
|
$opt = \Memcached::SERIALIZER_JSON; |
231
|
66 |
|
break; |
232
|
|
|
|
233
|
66 |
|
case 'json_array': |
234
|
66 |
|
if (!\Memcached::HAVE_JSON_ARRAY) { |
235
|
66 |
|
continue; |
236
|
66 |
|
} |
237
|
|
|
$opt = \Memcached::SERIALIZER_JSON_ARRAY; |
238
|
|
|
break; |
239
|
|
|
|
240
|
|
|
case 'msgpack': |
241
|
4 |
|
if (!\Memcached::HAVE_MSGPACK) { |
242
|
|
|
continue; |
243
|
4 |
|
} |
244
|
|
|
$opt = \Memcached::SERIALIZER_MSGPACK; |
245
|
|
|
break; |
246
|
|
|
// @codeCoverageIgnoreEnd |
247
|
|
|
|
248
|
|
|
default: |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
if (isset($opt)) { |
252
|
|
|
$this->adapter->setOption(\Memcached::OPT_SERIALIZER, $opt); |
253
|
|
|
} |
254
|
50 |
|
} |
255
|
|
|
|
256
|
50 |
|
/** |
257
|
50 |
|
* {@inheritdoc} |
258
|
36 |
|
*/ |
259
|
|
|
public function getSerializer() |
260
|
36 |
|
{ |
261
|
|
|
return $this->adapter->getOption(\Memcached::OPT_SERIALIZER); |
262
|
|
|
} |
263
|
25 |
|
|
264
|
|
|
/** |
265
|
|
|
* Retrieves the cache item for the given id. |
266
|
|
|
* |
267
|
|
|
* @param string $id The cache id to retrieve. |
268
|
|
|
* @param float $cas_token The variable to store the CAS token in. |
269
|
|
|
* |
270
|
|
|
* @return mixed|null Returns the cached data or null. |
271
|
|
|
*/ |
272
|
|
|
public function get($id, &$cas_token = null) |
273
|
|
|
{ |
274
|
|
|
$data = $this->adapter->get($id, null, $cas_token); |
275
|
|
|
if ($this->adapter->getResultCode() == \Memcached::RES_SUCCESS) { |
276
|
|
|
$this->ttls[$id] = isset($data['ttl']) ? $data['ttl'] : 0; |
277
|
36 |
|
|
278
|
|
|
return isset($data['data']) ? $data['data'] : $data; |
279
|
36 |
|
} |
280
|
|
|
|
281
|
|
|
return; |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* Returns the ttl sanitased for this cache adapter. |
286
|
|
|
* |
287
|
|
|
* The number of seconds may not exceed 60*60*24*30 = 2,592,000 (30 days). |
288
|
|
|
* |
289
|
66 |
|
* @see http://php.net/manual/en/memcached.expiration.php |
290
|
|
|
* |
291
|
66 |
|
* @param int|null $ttl The time-to-live in seconds. |
292
|
|
|
* |
293
|
|
|
* @return int |
294
|
|
|
*/ |
295
|
|
|
public function sanitiseTtl($ttl) |
296
|
|
|
{ |
297
|
|
|
return $ttl > 2592000 ? time() + $ttl : $ttl; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* Returns the named indexer. |
302
|
|
|
* |
303
|
|
|
* @param string $name The name of the index. |
304
|
66 |
|
* |
305
|
|
|
* @return Indexer\Adapter |
306
|
|
|
*/ |
307
|
66 |
|
public function getIndex($name) |
308
|
|
|
{ |
309
|
|
|
return new Indexer\MemcachedIndexer($name, $this); |
310
|
66 |
|
} |
311
|
|
|
|
312
|
66 |
|
/** |
313
|
|
|
* Sets the namespace prefix. |
314
|
4 |
|
* Specific to memcache; this sets as 'ns'+integer (incremented). |
315
|
4 |
|
* |
316
|
66 |
|
* @param string $ns |
317
|
66 |
|
* @param bool $renew |
318
|
66 |
|
* @param string $suffix |
319
|
66 |
|
* |
320
|
66 |
|
* @return int |
321
|
|
|
*/ |
322
|
|
|
public function setNamespace($ns, $renew = false, $suffix = '_') |
323
|
66 |
|
{ |
324
|
66 |
|
// temporally set the namespace to null |
325
|
|
|
$this->adapter->setOption(\Memcached::OPT_PREFIX_KEY, null); |
326
|
66 |
|
|
327
|
|
|
// mark the current namespace for future deletion |
328
|
|
|
$this->getIndex($this->mapIdx($ns))->remove($this->getNamespace()); |
329
|
|
|
|
330
|
|
|
if ($renew) { |
331
|
|
|
// increment the namespace counter |
332
|
|
|
$counter = $this->increment($ns); |
333
|
|
|
} else { |
334
|
66 |
|
$counter = $this->adapter->get($ns); |
335
|
|
|
if (false === $counter) { |
336
|
66 |
|
$counter = 1; |
337
|
|
|
$this->adapter->set($ns, $counter); |
338
|
|
|
} |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
$ns .= $counter.$suffix; |
342
|
|
|
$this->adapter->setOption(\Memcached::OPT_PREFIX_KEY, $ns); |
343
|
|
|
|
344
|
|
|
return $counter; |
345
|
|
|
} |
346
|
66 |
|
|
347
|
|
|
/** |
348
|
66 |
|
* Returns the namespace. |
349
|
|
|
* |
350
|
|
|
* @return string |
351
|
|
|
*/ |
352
|
|
|
public function getNamespace() |
353
|
|
|
{ |
354
|
|
|
return $this->adapter->getOption(\Memcached::OPT_PREFIX_KEY); |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
/** |
358
|
6 |
|
* Returns a prefixed and sanitased cache id. |
359
|
|
|
* |
360
|
|
|
* @param string $key The base key to prefix. |
361
|
|
|
* |
362
|
|
|
* @return string |
363
|
|
|
*/ |
364
|
|
|
public function mapIdx($key) |
365
|
6 |
|
{ |
366
|
6 |
|
return $this->sanitise($this->options['prefix_idx'].$key); |
367
|
2 |
|
} |
368
|
2 |
|
|
369
|
2 |
|
/** |
370
|
6 |
|
* Increments the value of the given key. |
371
|
|
|
* |
372
|
|
|
* @param string $key The key to increment. |
373
|
6 |
|
* |
374
|
|
|
* @return int|bool Returns the new item's value on success or FALSE on failure. |
375
|
|
|
*/ |
376
|
|
|
public function increment($key) |
377
|
|
|
{ |
378
|
|
|
// if (true === \Memcached::OPT_BINARY_PROTOCOL) { |
|
|
|
|
379
|
|
|
// // Increment will initialize the value (if not available) |
380
|
|
|
// // only when OPT_BINARY_PROTOCOL is set to true! |
381
|
|
|
// return $this->adapter->increment($key, 1); |
|
|
|
|
382
|
|
|
// } |
383
|
4 |
|
$counter = $this->adapter->get($key); |
384
|
|
|
if (false === $counter) { |
385
|
4 |
|
$counter = 1; |
386
|
|
|
$this->adapter->set($key, $counter); |
387
|
4 |
|
} else { |
388
|
4 |
|
$counter = $this->adapter->increment($key); |
389
|
4 |
|
} |
390
|
4 |
|
|
391
|
4 |
|
return $counter; |
392
|
4 |
|
} |
393
|
4 |
|
|
394
|
|
|
/** |
395
|
4 |
|
* {@inheritdoc} |
396
|
|
|
* |
397
|
|
|
* The number of seconds may not exceed 60*60*24*30 = 2,592,000 (30 days). |
398
|
2 |
|
* |
399
|
|
|
* @see http://php.net/manual/en/memcached.expiration.php |
400
|
|
|
*/ |
401
|
|
|
public function getTtl($key) |
402
|
|
|
{ |
403
|
|
|
$mKey = $this->mapKey($key); |
404
|
|
|
|
405
|
|
|
if (!isset($this->ttls[$mKey])) { |
406
|
|
|
$data = $this->adapter->get($mKey, null, $cas_token); |
407
|
|
|
$this->ttls[$mKey] = |
408
|
|
|
$this->adapter->getResultCode() == \Memcached::RES_SUCCESS |
409
|
|
|
? (isset($data['ttl']) ? $data['ttl'] : 0) |
410
|
|
|
: false; |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
return $this->ttls[$mKey]; |
414
|
|
|
} |
415
|
|
|
} |
416
|
|
|
|
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:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.