Completed
Push — master ( 6714de...59daff )
by Franck
08:55
created

Memcached::deleteMulti()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 17
ccs 9
cts 9
cp 1
rs 9.2
cc 4
eloc 8
nc 4
nop 1
crap 4
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 66
     * @param array      $options   Array of options.
42
     */
43
    public function __construct(\Memcached $memcached, array $options = null)
44 66
    {
45 66
        // default options
46 66
        $this->options['prefix_key'] = 'key_';  // prefix cache keys
47 66
        $this->options['prefix_tag'] = 'tag_';  // prefix cache tags
48
        $this->options['prefix_idx'] = 'idx_';  // prefix cache indexes
49 66
        $this->options['prefix_nsp'] = 'nsp_';  // prefix cache namespaces
50
51 66
        $this->options['serializer'] = 'php';   // none, php, json, igBinary.
52
53 66
        parent::__construct($memcached, $options);
54
55 66
        $memcached->setOption(\Memcached::OPT_COMPRESSION, false);
56 66
57 66
        if ($this->options['tag_enable']) {
58 66
            $memcached->setOption(\Memcached::OPT_BINARY_PROTOCOL, false);
59 66
            $this->setSerializer($this->options['serializer']);
60 66
            $this->setNamespace($this->options['prefix_nsp']);
61
        }
62
    }
63
64
    /**
65 18
     * {@inheritdoc}
66
     */
67 18
    public function loadKey($key)
68
    {
69
        return $this->get($this->mapKey($key));
70
    }
71
72
    /**
73 22
     * {@inheritdoc}
74
     */
75 22
    public function loadTag($tag)
76
    {
77
        return $this->getIndex($this->mapTag($tag))->load();
78
    }
79
80
    /**
81 36
     * {@inheritdoc}
82
     */
83 36
    public function save($data, $key, array $tags = null, $ttl = null)
84
    {
85 36
        $ttl = $this->sanitiseTtl($ttl);
86
87 36
        $mKey = $this->mapKey($key);
88 36
89
        $data = array('data' => $data, 'ttl' => $ttl);
90
        $this->ttls[$mKey] = $ttl;
91 36
92
        // add the item
93 36
        $success = $this->adapter->set($mKey, $data, $ttl);
94
95
        if ($success && $this->options['tag_enable'] && !empty($tags)) {
96 20
97
            // add all the tags to the index key.
98
            $this->getIndex($this->mapIdx($key))->add($tags);
99 20
100 20
            // append the key to each tag.
101 20
            foreach ($tags as $tag) {
102
                $this->getIndex($this->mapTag($tag))->add($mKey);
103 20
            }
104
        }
105 36
106
        return $success;
107
    }
108
109
    /**
110
     * Alias to `Memcached::deleteMulti` or loop `Memcached::delete`.
111 2
     *
112
     * @param array $items The items to be deleted.
113 2
     *
114 2
     * @return bool Returns TRUE on success or FALSE on failure.
115 2
     */
116 2
    protected function deleteMulti($items)
117 2
    {
118 2
        if (method_exists($this->adapter, 'deleteMulti')) {
119
            $this->adapter->deleteMulti($items);
120 2
121 2
            return (boolean) $this->adapter->getResultCode() != \Memcached::RES_FAILURE;
122
        }
123 2
124
        // Fix environments (some HHVM versions) that don't handle deleteMulti.
125
        // @see https://github.com/facebook/hhvm/issues/4602
126
        $success = true;
127 2
        foreach ($items as $item) {
128
            $success = $this->adapter->delete($item) && $success;
129 2
        }
130
131 2
        return $success;
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137 6
    public function clean(array $tags)
138
    {
139 6
        $items = array();
140 6
        foreach ($tags as $tag) {
141
            $keys = $this->loadTag($tag);
142 6
            if (null !== $keys) {
143 4
                foreach ($keys as $key) {
144
                    $items[] = $key;
145
                    // $items[] = $this->mapIdx($key);
146 4
                }
147
            }
148 4
            // add the tag to deletion
149
            $items[] = $this->mapTag($tag);
150 2
151 2
            // add the index key for deletion
152 2
            // $items[] = $this->mapTag($tag);
153
        }
154 2
155 2
        return $this->deleteMulti($items);
156 4
    }
157 6
158
    /**
159 6
     * {@inheritdoc}
160
     */
161
    public function delete($key)
162
    {
163
        $_key = $this->mapKey($key);
164
        $items = array($_key);
165 66
166
        if ($this->options['tag_enable']) {
167 66
            $idx_key = $this->mapIdx($key);
168 66
169
            // load the tags from the index key
170 4
            $tags = $this->getIndex($idx_key)->load();
171
172
            if (is_array($tags)) {
173 4
                // mark the key as deleted in the tags.
174
                foreach ($tags as $tag) {
175 4
                    $this->getIndex($this->mapTag($tag))->remove($_key);
176
                }
177
                // delete that index key
178
                $items[] = $idx_key;
179
            }
180
        }
181
182 66
        return $this->deleteMulti($items);
183
    }
184
185
    /**
186
     * {@inheritdoc}
187
     */
188
    public function flush($all = false)
189
    {
190
        if (true === $all) {
191
            return $this->adapter->flush();
192
        }
193
        $nsKey = $this->options['prefix_nsp'];
194
195
        // set a new namespace
196
        $success = $this->setNamespace($nsKey, true);
197
198
        return (boolean) $success;
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     *
204 66
     * @param string $serializer
205 66
     */
206 66
    public function setSerializer($serializer)
207 66
    {
208
        switch ($serializer) {
209 66
210 66
            // @codeCoverageIgnoreStart
211 66
            case 'igBinary':
212 66
                if (!\Memcached::HAVE_IGBINARY) {
213
                    continue;
214
                }
215
                $opt = \Memcached::SERIALIZER_IGBINARY;
216
            break;
217 4
            // @codeCoverageIgnoreEnd
218
219 4
            // @codeCoverageIgnoreStart
220
            case 'json':
221
                if (!\Memcached::HAVE_JSON) {
222
                    continue;
223
                }
224
                $opt = \Memcached::SERIALIZER_JSON;
225
            break;
226
            // @codeCoverageIgnoreEnd
227
228
            case 'php':
229 50
            default:
230
                $opt = \Memcached::SERIALIZER_PHP;
231 50
        }
232 50
233 36
        if (isset($opt)) {
234
            $this->adapter->setOption(\Memcached::OPT_SERIALIZER, $opt);
235 36
        }
236
    }
237
238 25
    /**
239
     * {@inheritdoc}
240
     */
241
    public function getSerializer()
242
    {
243
        return $this->adapter->getOption(\Memcached::OPT_SERIALIZER);
244
    }
245
246
    /**
247
     * Retrieves the cache item for the given id.
248
     *
249
     * @param string $id        The cache id to retrieve.
250 36
     * @param float  $cas_token The variable to store the CAS token in.
251
     *
252 36
     * @return mixed|null Returns the cached data or null.
253
     */
254
    public function get($id, &$cas_token = null)
255
    {
256
        $data = $this->adapter->get($id, null, $cas_token);
257
        if ($this->adapter->getResultCode() == \Memcached::RES_SUCCESS) {
258
            $this->ttls[$id] = isset($data['ttl']) ? $data['ttl'] : 0;
259
260
            return isset($data['data']) ? $data['data'] : $data;
261 66
        }
262
263 66
        return;
264
    }
265
266
    /**
267
     * Returns the ttl sanitased for this cache adapter.
268
     *
269
     * The number of seconds may not exceed 60*60*24*30 = 2,592,000 (30 days).
270
     *
271
     * @see http://php.net/manual/en/memcached.expiration.php
272
     *
273
     * @param int|null $ttl The time-to-live in seconds.
274
     *
275 66
     * @return int
276
     */
277
    public function sanitiseTtl($ttl)
278 66
    {
279
        return $ttl > 2592000 ? time() + $ttl : $ttl;
280
    }
281 66
282
    /**
283 66
     * Returns the named indexer.
284
     *
285 4
     * @param string $name The name of the index.
286 4
     *
287 66
     * @return Indexer\Adapter
288 66
     */
289 66
    public function getIndex($name)
290 66
    {
291 66
        return new Indexer\MemcachedIndexer($name, $this);
292
    }
293
294 66
    /**
295 66
     * Sets the namespace prefix.
296
     * Specific to memcache; this sets as 'ns'+integer (incremented).
297 66
     *
298
     * @param string $ns
299
     * @param bool   $renew
300
     * @param string $suffix
301
     *
302
     * @return int
303
     */
304
    public function setNamespace($ns, $renew = false, $suffix = '_')
305 66
    {
306
        // temporally set the namespace to null
307 66
        $this->adapter->setOption(\Memcached::OPT_PREFIX_KEY, null);
308
309
        // mark the current namespace for future deletion
310
        $this->getIndex($this->mapIdx($ns))->remove($this->getNamespace());
311
312
        if ($renew) {
313
            // increment the namespace counter
314
            $counter = $this->increment($ns);
315
        } else {
316 66
            $counter = $this->adapter->get($ns);
317
            if (false === $counter) {
318 66
                $counter = 1;
319
                $this->adapter->set($ns, $counter);
320
            }
321
        }
322
323
        $ns .= $counter.$suffix;
324
        $this->adapter->setOption(\Memcached::OPT_PREFIX_KEY, $ns);
325
326
        return $counter;
327 6
    }
328
329
    /**
330
     * Returns the namespace.
331
     *
332
     * @return string
333
     */
334 6
    public function getNamespace()
335 6
    {
336 2
        return $this->adapter->getOption(\Memcached::OPT_PREFIX_KEY);
337 2
    }
338 2
339 6
    /**
340
     * Returns a prefixed and sanitased cache id.
341
     *
342 6
     * @param string $key The base key to prefix.
343
     *
344
     * @return string
345
     */
346
    public function mapIdx($key)
347
    {
348
        return $this->sanitise($this->options['prefix_idx'].$key);
349
    }
350
351 4
    /**
352
     * Increments the value of the given key.
353 4
     *
354
     * @param string $key The key to increment.
355 4
     *
356 4
     * @return int|bool Returns the new item's value on success or FALSE on failure.
357 4
     */
358 4
    public function increment($key)
359 4
    {
360 4
        // if (true === \Memcached::OPT_BINARY_PROTOCOL) {
361 4
        //     // Increment will initialize the value (if not available)
362
        //     // only when OPT_BINARY_PROTOCOL is set to true!
363 4
        //     return $this->adapter->increment($key, 1);
364
        // }
365
        $counter = $this->adapter->get($key);
366
        if (false === $counter) {
367 2
            $counter = 1;
368
            $this->adapter->set($key, $counter);
369
        } else {
370
            $counter = $this->adapter->increment($key);
371
        }
372
373
        return $counter;
374
    }
375
376
    /**
377
     * {@inheritdoc}
378
     *
379
     * The number of seconds may not exceed 60*60*24*30 = 2,592,000 (30 days).
380
     *
381
     * @see http://php.net/manual/en/memcached.expiration.php
382
     */
383
    public function getTtl($key)
384
    {
385
        $mKey = $this->mapKey($key);
386
387
        if (!isset($this->ttls[$mKey])) {
388
            $data = $this->adapter->get($mKey, null, $cas_token);
389
            $this->ttls[$mKey] =
390
                 $this->adapter->getResultCode() == \Memcached::RES_SUCCESS
391
                    ? (isset($data['ttl']) ? $data['ttl'] : 0)
392
                    : false;
393
        }
394
395
        return $this->ttls[$mKey];
396
    }
397
}
398