Completed
Push — master ( 45c10a...9b310e )
by Franck
08:21
created

Memcached::deleteMulti()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 6.7441

Importance

Changes 2
Bugs 2 Features 0
Metric Value
c 2
b 2
f 0
dl 0
loc 17
ccs 4
cts 9
cp 0.4444
rs 9.2
cc 4
eloc 8
nc 4
nop 1
crap 6.7441
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
        $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
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 66
        }
62 66
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67 18
    public function loadKey($key)
68
    {
69 18
        return $this->get($this->mapKey($key));
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75 22
    public function loadTag($tag)
76
    {
77 22
        return $this->getIndex($this->mapTag($tag))->load();
78
    }
79
80
    /**
81
     * {@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
89 36
        $data = array('data' => $data, 'ttl' => $ttl);
90 36
        $this->ttls[$mKey] = $ttl;
91
92
        // add the item
93 36
        $success = $this->adapter->set($mKey, $data, $ttl);
94
95 36
        if ($success && $this->options['tag_enable'] && !empty($tags)) {
96
97
            // add all the tags to the index key.
98 20
            $this->getIndex($this->mapIdx($key))->add($tags);
99
100
            // append the key to each tag.
101 20
            foreach ($tags as $tag) {
102 20
                $this->getIndex($this->mapTag($tag))->add($mKey);
103 20
            }
104 20
        }
105
106 36
        return $success;
107
    }
108
109
    /**
110
     * Alias to `Memcached::deleteMulti` or loop `Memcached::delete`.
111
     *
112
     * @param array $items The items to be deleted.
113
     *
114
     * @return bool Returns TRUE on success or FALSE on failure.
115
     */
116 8
    protected function deleteMulti($items)
117
    {
118 8
        if (method_exists($this->adapter, 'deleteMulti')) {
119 8
            $this->adapter->deleteMulti($items);
120
121 8
            return (boolean) $this->adapter->getResultCode() != \Memcached::RES_FAILURE;
122
        }
123
124
        // Fix environments (some HHVM versions) that don't handle deleteMulti.
125
        // @see https://github.com/facebook/hhvm/issues/4602
126
        $success = true;
127
        foreach ($items as $item) {
128
            $success = $this->adapter->delete($item) && $success;
129
        }
130
131
        return $success;
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137 2
    public function clean(array $tags)
138
    {
139 2
        $items = array();
140 2
        foreach ($tags as $tag) {
141 2
            $keys = $this->loadTag($tag);
142 2
            if (null !== $keys) {
143 2
                foreach ($keys as $key) {
144 2
                    $items[] = $key;
145
                    // $items[] = $this->mapIdx($key);
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
146 2
                }
147 2
            }
148
            // add the tag to deletion
149 2
            $items[] = $this->mapTag($tag);
150
151
            // add the index key for deletion
152
            // $items[] = $this->mapTag($tag);
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
153 2
        }
154
155 2
        return $this->deleteMulti($items);
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161 6
    public function delete($key)
162
    {
163 6
        $_key = $this->mapKey($key);
164 6
        $items = array($_key);
165
166 6
        if ($this->options['tag_enable']) {
167 4
            $idx_key = $this->mapIdx($key);
168
169
            // load the tags from the index key
170 4
            $tags = $this->getIndex($idx_key)->load();
171
172 4
            if (is_array($tags)) {
173
                // mark the key as deleted in the tags.
174 2
                foreach ($tags as $tag) {
175 2
                    $this->getIndex($this->mapTag($tag))->remove($_key);
176 2
                }
177
                // delete that index key
178 2
                $items[] = $idx_key;
179 2
            }
180 4
        }
181
182 6
        return $this->deleteMulti($items);
183
    }
184
185
    /**
186
     * {@inheritdoc}
187
     */
188 66
    public function flush($all = false)
189
    {
190 66
        if (true === $all) {
191 66
            return $this->adapter->flush();
192
        }
193 4
        $nsKey = $this->options['prefix_nsp'];
194
195
        // set a new namespace
196 4
        $success = $this->setNamespace($nsKey, true);
197
198 4
        return (boolean) $success;
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     *
204
     * @param string $serializer
205
     */
206 66
    public function setSerializer($serializer)
207
    {
208
        switch ($serializer) {
209
210
            // @codeCoverageIgnoreStart
211
            case 'igBinary':
212
                if (!\Memcached::HAVE_IGBINARY) {
213
                    continue;
214
                }
215
                $opt = \Memcached::SERIALIZER_IGBINARY;
216
            break;
217
            // @codeCoverageIgnoreEnd
218
219
            // @codeCoverageIgnoreStart
220
            case 'json':
221
                if (!\Memcached::HAVE_JSON) {
222
                    continue;
223
                }
224
                $opt = \Memcached::SERIALIZER_JSON;
225
            break;
226
            // @codeCoverageIgnoreEnd
227
228 66
            case 'php':
229 66
            default:
230 66
                $opt = \Memcached::SERIALIZER_PHP;
231 66
        }
232
233 66
        if (isset($opt)) {
234 66
            $this->adapter->setOption(\Memcached::OPT_SERIALIZER, $opt);
235 66
        }
236 66
    }
237
238
    /**
239
     * {@inheritdoc}
240
     */
241 4
    public function getSerializer()
242
    {
243 4
        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
     * @param float  $cas_token The variable to store the CAS token in.
251
     *
252
     * @return mixed|null Returns the cached data or null.
253
     */
254 50
    public function get($id, &$cas_token = null)
255
    {
256 50
        $data = $this->adapter->get($id, null, $cas_token);
257 50
        if ($this->adapter->getResultCode() == \Memcached::RES_SUCCESS) {
258 36
            $this->ttls[$id] = isset($data['ttl']) ? $data['ttl'] : 0;
259
260 36
            return isset($data['data']) ? $data['data'] : $data;
261
        }
262
263 25
        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
     * @return int
276
     */
277 36
    public function sanitiseTtl($ttl)
278
    {
279 36
        return $ttl > 2592000 ? time() + $ttl : $ttl;
280
    }
281
282
    /**
283
     * Returns the named indexer.
284
     *
285
     * @param string $name The name of the index.
286
     *
287
     * @return Indexer\Adapter
288
     */
289 66
    public function getIndex($name)
290
    {
291 66
        return new Indexer\MemcachedIndexer($name, $this);
292
    }
293
294
    /**
295
     * Sets the namespace prefix.
296
     * Specific to memcache; this sets as 'ns'+integer (incremented).
297
     *
298
     * @param string $ns
299
     * @param bool   $renew
300
     * @param string $suffix
301
     *
302
     * @return int
303
     */
304 66
    public function setNamespace($ns, $renew = false, $suffix = '_')
305
    {
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 66
        $this->getIndex($this->mapIdx($ns))->remove($this->getNamespace());
311
312 66
        if ($renew) {
313
            // increment the namespace counter
314 4
            $counter = $this->increment($ns);
315 4
        } else {
316 66
            $counter = $this->adapter->get($ns);
317 66
            if (false === $counter) {
318 66
                $counter = 1;
319 66
                $this->adapter->set($ns, $counter);
320 66
            }
321
        }
322
323 66
        $ns .= $counter.$suffix;
324 66
        $this->adapter->setOption(\Memcached::OPT_PREFIX_KEY, $ns);
325
326 66
        return $counter;
327
    }
328
329
    /**
330
     * Returns the namespace.
331
     *
332
     * @return string
333
     */
334 66
    public function getNamespace()
335
    {
336 66
        return $this->adapter->getOption(\Memcached::OPT_PREFIX_KEY);
337
    }
338
339
    /**
340
     * Returns a prefixed and sanitased cache id.
341
     *
342
     * @param string $key The base key to prefix.
343
     *
344
     * @return string
345
     */
346 66
    public function mapIdx($key)
347
    {
348 66
        return $this->sanitise($this->options['prefix_idx'].$key);
349
    }
350
351
    /**
352
     * Increments the value of the given key.
353
     *
354
     * @param string $key The key to increment.
355
     *
356
     * @return int|bool Returns the new item's value on success or FALSE on failure.
357
     */
358 6
    public function increment($key)
359
    {
360
        // if (true === \Memcached::OPT_BINARY_PROTOCOL) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
361
        //     // Increment will initialize the value (if not available)
362
        //     // only when OPT_BINARY_PROTOCOL is set to true!
363
        //     return $this->adapter->increment($key, 1);
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
364
        // }
365 6
        $counter = $this->adapter->get($key);
366 6
        if (false === $counter) {
367 2
            $counter = 1;
368 2
            $this->adapter->set($key, $counter);
369 2
        } else {
370 6
            $counter = $this->adapter->increment($key);
371
        }
372
373 6
        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 4
    public function getTtl($key)
384
    {
385 4
        $mKey = $this->mapKey($key);
386
387 4
        if (!isset($this->ttls[$mKey])) {
388 4
            $data = $this->adapter->get($mKey, null, $cas_token);
389 4
            $this->ttls[$mKey] =
390 4
                 $this->adapter->getResultCode() == \Memcached::RES_SUCCESS
391 4
                    ? (isset($data['ttl']) ? $data['ttl'] : 0)
392 4
                    : false;
393 4
        }
394
395 4
        return $this->ttls[$mKey];
396
    }
397
}
398