Completed
Push — master ( 2fc7da...d5a1de )
by Matthias
09:40
created

Memcached::getMulti()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 30
rs 8.439
c 0
b 0
f 0
cc 6
eloc 17
nc 9
nop 2
1
<?php
2
3
namespace MatthiasMullie\Scrapbook\Adapters;
4
5
use MatthiasMullie\Scrapbook\Adapters\Collections\Memcached as Collection;
6
use MatthiasMullie\Scrapbook\Exception\InvalidKey;
7
use MatthiasMullie\Scrapbook\KeyValueStore;
8
9
/**
10
 * Memcached adapter. Basically just a wrapper over \Memcached, but in an
11
 * exchangeable (KeyValueStore) interface.
12
 *
13
 * @author Matthias Mullie <[email protected]>
14
 * @copyright Copyright (c) 2014, Matthias Mullie. All rights reserved
15
 * @license LICENSE MIT
16
 */
17
class Memcached implements KeyValueStore
18
{
19
    /**
20
     * @var Memcached
21
     */
22
    protected $client;
23
24
    /**
25
     * @param \Memcached $client
26
     */
27
    public function __construct(\Memcached $client)
28
    {
29
        $this->client = $client;
0 ignored issues
show
Documentation Bug introduced by Matthias Mullie
It seems like $client of type object<Memcached> is incompatible with the declared type object<MatthiasMullie\Sc...ook\Adapters\Memcached> of property $client.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
30
    }
31
32
    /**
33
     * {@inheritdoc}
34
     */
35
    public function get($key, &$token = null)
36
    {
37
        /*
38
         * Wouldn't it be awesome if I just didn't use the obvious method? :)
39
         *
40
         * I'm going to use getMulti() instead of get() because the latter is
41
         * flawed in earlier versions, where it was known to mess up some
42
         * operations that are followed by it (increment/decrement have been
43
         * reported, also seen it make CAS return result unreliable)
44
         * @see https://github.com/php-memcached-dev/php-memcached/issues/21
45
         */
46
        $values = $this->getMulti(array($key), $tokens);
47
48
        if (!isset($values[$key])) {
49
            $token = null;
50
51
            return false;
52
        }
53
54
        $token = $tokens[$key];
55
56
        return $values[$key];
57
    }
58
59
    /**
60
     * {@inheritdoc}
61
     */
62
    public function getMulti(array $keys, array &$tokens = null)
63
    {
64
        $tokens = array();
65
        if (empty($keys)) {
66
            return array();
67
        }
68
69
        $keys = array_map(array($this, 'encode'), $keys);
70
71
        if (defined('\Memcached::GET_EXTENDED')) {
72
            $return = $this->client->getMulti($keys, \Memcached::GET_EXTENDED);
0 ignored issues
show
Bug introduced by Matthias Mullie
\Memcached::GET_EXTENDED cannot be passed to getmulti() as the parameter $tokens expects a reference.
Loading history...
73
            foreach ($return as $key => $value) {
74
                // once PHP<5.5 support is dropped, just use array_column
75
                $tokens[$key] = $value['cas'];
76
                $return[$key] = $value['value'];
77
            }
78
        } else {
79
            $return = $this->client->getMulti($keys, $tokens);
80
        }
81
82
        $keys = array_map(array($this, 'decode'), array_keys($return));
83
        $return = array_combine($keys, $return);
84
85
        // HHVMs getMulti() returns null instead of empty array for no results,
86
        // so normalize that
87
        $tokens = $tokens ?: array();
88
        $tokens = array_combine($keys, $tokens);
89
90
        return $return ?: array();
91
    }
92
93
    /**
94
     * {@inheritdoc}
95
     */
96
    public function set($key, $value, $expire = 0)
97
    {
98
        // Memcached seems to not timely purge items the way it should when
99
        // storing it with an expired timestamp
100 View Code Duplication
        if ($expire < 0 || ($expire > 2592000 && $expire < time())) {
0 ignored issues
show
Duplication introduced by Matthias Mullie
This code seems to be duplicated across your project.

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.

Loading history...
101
            $this->delete($key);
102
103
            return true;
104
        }
105
106
        $key = $this->encode($key);
107
108
        return $this->client->set($key, $value, $expire);
109
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114
    public function setMulti(array $items, $expire = 0)
115
    {
116
        if (empty($items)) {
117
            return array();
118
        }
119
120
        // Memcached seems to not timely purge items the way it should when
121
        // storing it with an expired timestamp
122 View Code Duplication
        if ($expire < 0 || ($expire > 2592000 && $expire < time())) {
0 ignored issues
show
Duplication introduced by Matthias Mullie
This code seems to be duplicated across your project.

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.

Loading history...
123
            $keys = array_keys($items);
124
            $this->deleteMulti($keys);
125
126
            return array_fill_keys($keys, true);
127
        }
128
129
        /*
130
         * Numerical strings turn into integers when used as array keys, and
131
         * HHVM (used to) reject(s) such cache keys.
132
         *
133
         * @see https://github.com/facebook/hhvm/pull/7654
134
         */
135 View Code Duplication
        if (defined('HHVM_VERSION')) {
0 ignored issues
show
Duplication introduced by Matthias Mullie
This code seems to be duplicated across your project.

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.

Loading history...
136
            $nums = array_filter(array_keys($items), 'is_numeric');
137
            if ($nums) {
0 ignored issues
show
Bug Best Practice introduced by Matthias Mullie
The expression $nums of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
138
                $success = [];
139
                $nums = array_intersect_key($items, array_fill_keys($nums, null));
140
                foreach ($nums as $k => $v) {
141
                    $success[$k] = $this->set((string) $k, $v, $expire);
142
                }
143
144
                $remaining = array_diff_key($items, $nums);
145
                if ($remaining) {
0 ignored issues
show
Bug Best Practice introduced by Matthias Mullie
The expression $remaining of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
146
                    $success += $this->setMulti($remaining, $expire);
147
                }
148
149
                return $success;
150
            }
151
        }
152
153
        $keys = array_map(array($this, 'encode'), array_keys($items));
154
        $items = array_combine($keys, $items);
155
        $success = $this->client->setMulti($items, $expire);
156
        $keys = array_map(array($this, 'decode'), array_keys($items));
157
158
        return array_fill_keys($keys, $success);
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164
    public function delete($key)
165
    {
166
        $key = $this->encode($key);
167
168
        return $this->client->delete($key);
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     */
174
    public function deleteMulti(array $keys)
175
    {
176
        if (empty($keys)) {
177
            return array();
178
        }
179
180
        if (!method_exists($this->client, 'deleteMulti')) {
181
            /*
182
             * HHVM didn't always support deleteMulti, so I'll hack around it by
183
             * setting all items expired.
184
             * I could also delete() all items one by one, but that would
185
             * probably take more network requests (this version always takes 2)
186
             *
187
             * @see http://docs.hhvm.com/manual/en/memcached.deletemulti.php
188
             */
189
            $values = $this->getMulti($keys);
190
191
            $keys = array_map(array($this, 'encode'), array_keys($values));
192
            $this->client->setMulti(array_fill_keys($keys, ''), time() - 1);
193
194
            $return = array();
195
            foreach ($keys as $key) {
196
                $key = $this->decode($key);
197
                $return[$key] = array_key_exists($key, $values);
198
            }
199
200
            return $return;
201
        }
202
203
        $keys = array_map(array($this, 'encode'), $keys);
204
        $result = (array) $this->client->deleteMulti($keys);
205
        $keys = array_map(array($this, 'decode'), array_keys($result));
206
        $result = array_combine($keys, $result);
207
208
        /*
209
         * Contrary to docs (http://php.net/manual/en/memcached.deletemulti.php)
210
         * deleteMulti returns an array of [key => true] (for successfully
211
         * deleted values) and [key => error code] (for failures)
212
         * Pretty good because I want an array of true/false, so I'll just have
213
         * to replace the error codes by falses.
214
         */
215
        foreach ($result as $key => $status) {
216
            $result[$key] = $status === true;
217
        }
218
219
        return $result;
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225
    public function add($key, $value, $expire = 0)
226
    {
227
        $key = $this->encode($key);
228
229
        return $this->client->add($key, $value, $expire);
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     */
235
    public function replace($key, $value, $expire = 0)
236
    {
237
        $key = $this->encode($key);
238
239
        return $this->client->replace($key, $value, $expire);
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245 View Code Duplication
    public function cas($token, $key, $value, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

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.

Loading history...
246
    {
247
        if (!is_float($token)) {
248
            return false;
249
        }
250
251
        $key = $this->encode($key);
252
253
        return $this->client->cas($token, $key, $value, $expire);
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259 View Code Duplication
    public function increment($key, $offset = 1, $initial = 0, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

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.

Loading history...
260
    {
261
        if ($offset <= 0 || $initial < 0) {
262
            return false;
263
        }
264
265
        /*
266
         * Not doing \Memcached::increment because that one:
267
         * * needs \Memcached::OPT_BINARY_PROTOCOL == true
268
         * * is prone to errors after a flush ("merges" with pruned data) in at
269
         *   least some particular versions of Memcached
270
         */
271
        return $this->doIncrement($key, $offset, $initial, $expire);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression $this->doIncrement($key,...et, $initial, $expire); of type integer|false|double adds the type double to the return on line 271 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::increment of type integer|boolean.
Loading history...
272
    }
273
274
    /**
275
     * {@inheritdoc}
276
     */
277 View Code Duplication
    public function decrement($key, $offset = 1, $initial = 0, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

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.

Loading history...
278
    {
279
        if ($offset <= 0 || $initial < 0) {
280
            return false;
281
        }
282
283
        /*
284
         * Not doing \Memcached::decrement for the reasons described in:
285
         * @see increment()
286
         */
287
        return $this->doIncrement($key, -$offset, $initial, $expire);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression $this->doIncrement($key,...et, $initial, $expire); of type integer|false|double adds the type double to the return on line 287 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::decrement of type integer|boolean.
Loading history...
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293
    public function touch($key, $expire)
294
    {
295
        /*
296
         * Since \Memcached has no reliable touch(), we might as well take an
297
         * easy approach where we can. If TTL is expired already, just delete
298
         * the key - this only needs 1 request.
299
         */
300 View Code Duplication
        if ($expire < 0 || ($expire > 2592000 && $expire < time())) {
0 ignored issues
show
Duplication introduced by Matthias Mullie
This code seems to be duplicated across your project.

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.

Loading history...
301
            return $this->delete($key);
302
        }
303
304
        /*
0 ignored issues
show
Unused Code Comprehensibility introduced by Matthias Mullie
40% 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...
305
         * HHVM doesn't support touch.
306
         * @see http://docs.hhvm.com/manual/en/memcached.touch.php
307
         *
308
         * PHP does, but only with \Memcached::OPT_BINARY_PROTOCOL == true,
309
         * and even then, it appears to be buggy on particular versions of
310
         * Memcached.
311
         *
312
         * I'll just work around it!
313
         */
314
        $value = $this->get($key, $token);
315
316
        return $this->cas($token, $key, $value, $expire);
317
    }
318
319
    /**
320
     * {@inheritdoc}
321
     */
322
    public function flush()
323
    {
324
        return $this->client->flush();
325
    }
326
327
    /**
328
     * {@inheritdoc}
329
     */
330
    public function getCollection($name)
331
    {
332
        return new Collection($this, $name);
333
    }
334
335
    /**
336
     * Shared between increment/decrement: both have mostly the same logic
337
     * (decrement just increments a negative value), but need their validation
338
     * split up (increment won't accept negative values).
339
     *
340
     * @param string $key
341
     * @param int    $offset
342
     * @param int    $initial
343
     * @param int    $expire
344
     *
345
     * @return int|bool
346
     */
347 View Code Duplication
    protected function doIncrement($key, $offset, $initial, $expire)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

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.

Loading history...
348
    {
349
        $value = $this->get($key, $token);
350
        if ($value === false) {
351
            $success = $this->add($key, $initial, $expire);
352
353
            return $success ? $initial : false;
354
        }
355
356
        if (!is_numeric($value) || $value < 0) {
357
            return false;
358
        }
359
360
        $value += $offset;
361
        // value can never be lower than 0
362
        $value = max(0, $value);
363
        $key = $this->encode($key);
364
        $success = $this->client->cas($token, $key, $value, $expire);
365
366
        return $success ? $value : false;
367
    }
368
369
    /**
370
     * Encode a key for use on the wire inside the memcached protocol.
371
     *
372
     * We encode spaces and line breaks to avoid protocol errors. We encode
373
     * the other control characters for compatibility with libmemcached
374
     * verify_key. We leave other punctuation alone, to maximise backwards
375
     * compatibility.
376
     *
377
     * @see https://github.com/wikimedia/mediawiki/commit/be76d869#diff-75b7c03970b5e43de95ff95f5faa6ef1R100
378
     * @see https://github.com/wikimedia/mediawiki/blob/master/includes/libs/objectcache/MemcachedBagOStuff.php#L116
379
     *
380
     * @param string $key
381
     *
382
     * @return string
383
     *
384
     * @throws InvalidKey
385
     */
386
    protected function encode($key)
387
    {
388
        $regex = '/[^\x21\x22\x24\x26-\x39\x3b-\x7e]+/';
389
        $key = preg_replace_callback($regex, function ($match) {
390
            return rawurlencode($match[0]);
391
        }, $key);
392
393
        if (strlen($key) > 255) {
394
            throw new InvalidKey(
395
                "Invalid key: $key. Encoded Memcached keys can not exceed 255 chars."
396
            );
397
        }
398
399
        return $key;
400
    }
401
402
    /**
403
     * Decode a key encoded with encode().
404
     *
405
     * @param string $key
406
     *
407
     * @return string
408
     */
409
    protected function decode($key)
410
    {
411
        // matches %20, %7F, ... but not %21, %22, ...
0 ignored issues
show
Unused Code Comprehensibility introduced by Matthias Mullie
38% 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...
412
        // (=the encoded versions for those encoded in encode)
413
        $regex = '/%(?!2[1246789]|3[0-9]|3[B-F]|[4-6][0-9A-F]|5[0-9A-E])[0-9A-Z]{2}/i';
414
415
        return preg_replace_callback($regex, function ($match) {
416
            return rawurldecode($match[0]);
417
        }, $key);
418
    }
419
}
420