Completed
Push — master ( 6fe9e3...708c75 )
by Matthias
02:50
created

Memcached::setMulti()   C

Complexity

Conditions 9
Paths 8

Size

Total Lines 46
Code Lines 23

Duplication

Lines 23
Ratio 50 %

Importance

Changes 0
Metric Value
dl 23
loc 46
rs 5.0942
c 0
b 0
f 0
cc 9
eloc 23
nc 8
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
        $key = $this->encode($key);
38
39
        /*
40
         * Wouldn't it be awesome if I just didn't use the obvious method? :)
41
         *
42
         * I'm going to use getMulti() instead of get() because the latter is
43
         * flawed in earlier versions, where it was known to mess up some
44
         * operations that are followed by it (increment/decrement have been
45
         * reported, also seen it make CAS return result unreliable)
46
         * @see https://github.com/php-memcached-dev/php-memcached/issues/21
47
         */
48
        $values = $this->client->getMulti(array($key), $token);
49
50
        if (!isset($values[$key])) {
51
            $token = null;
52
53
            return false;
54
        }
55
56
        $token = $token[$key];
57
58
        return $values[$key];
59
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64
    public function getMulti(array $keys, array &$tokens = null)
65
    {
66
        if (empty($keys)) {
67
            return array();
68
        }
69
70
        $keys = array_map(array($this, 'encode'), $keys);
71
        $return = $this->client->getMulti($keys, $tokens);
72
        $keys = array_map(array($this, 'decode'), array_keys($return));
73
        $return = array_combine($keys, $return);
74
75
        // HHVMs getMulti() returns null instead of empty array for no results,
76
        // so normalize that
77
        $tokens = $tokens ?: array();
78
        $tokens = array_combine($keys, $tokens);
79
80
        return $return ?: array();
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86
    public function set($key, $value, $expire = 0)
87
    {
88
        // Memcached seems to not timely purge items the way it should when
89
        // storing it with an expired timestamp
90 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...
91
            $this->delete($key);
92
93
            return true;
94
        }
95
96
        $key = $this->encode($key);
97
98
        return $this->client->set($key, $value, $expire);
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104
    public function setMulti(array $items, $expire = 0)
105
    {
106
        if (empty($items)) {
107
            return array();
108
        }
109
110
        // Memcached seems to not timely purge items the way it should when
111
        // storing it with an expired timestamp
112 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...
113
            $keys = array_keys($items);
114
            $this->deleteMulti($keys);
115
116
            return array_fill_keys($keys, true);
117
        }
118
119
        /*
120
         * Numerical strings turn into integers when used as array keys, and
121
         * HHVM (used to) reject(s) such cache keys.
122
         *
123
         * @see https://github.com/facebook/hhvm/pull/7654
124
         */
125 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...
126
            $nums = array_filter(array_keys($items), 'is_numeric');
127
            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...
128
                $success = [];
129
                $nums = array_intersect_key($items, array_fill_keys($nums, null));
130
                foreach ($nums as $k => $v) {
131
                    $success[$k] = $this->set((string) $k, $v, $expire);
132
                }
133
134
                $remaining = array_diff_key($items, $nums);
135
                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...
136
                    $success += $this->setMulti($remaining, $expire);
137
                }
138
139
                return $success;
140
            }
141
        }
142
143
        $keys = array_map(array($this, 'encode'), array_keys($items));
144
        $items = array_combine($keys, $items);
145
        $success = $this->client->setMulti($items, $expire);
146
        $keys = array_map(array($this, 'decode'), array_keys($items));
147
148
        return array_fill_keys($keys, $success);
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     */
154
    public function delete($key)
155
    {
156
        $key = $this->encode($key);
157
158
        return $this->client->delete($key);
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164
    public function deleteMulti(array $keys)
165
    {
166
        if (empty($keys)) {
167
            return array();
168
        }
169
170
        if (!method_exists($this->client, 'deleteMulti')) {
171
            /*
172
             * HHVM didn't always support deleteMulti, so I'll hack around it by
173
             * setting all items expired.
174
             * I could also delete() all items one by one, but that would
175
             * probably take more network requests (this version always takes 2)
176
             *
177
             * @see http://docs.hhvm.com/manual/en/memcached.deletemulti.php
178
             */
179
            $values = $this->getMulti($keys);
180
181
            $keys = array_map(array($this, 'encode'), array_keys($values));
182
            $this->client->setMulti(array_fill_keys($keys, ''), time() - 1);
183
184
            $return = array();
185
            foreach ($keys as $key) {
186
                $key = $this->decode($key);
187
                $return[$key] = array_key_exists($key, $values);
188
            }
189
190
            return $return;
191
        }
192
193
        $keys = array_map(array($this, 'encode'), $keys);
194
        $result = (array) $this->client->deleteMulti($keys);
195
        $keys = array_map(array($this, 'decode'), array_keys($result));
196
        $result = array_combine($keys, $result);
197
198
        /*
199
         * Contrary to docs (http://php.net/manual/en/memcached.deletemulti.php)
200
         * deleteMulti returns an array of [key => true] (for successfully
201
         * deleted values) and [key => error code] (for failures)
202
         * Pretty good because I want an array of true/false, so I'll just have
203
         * to replace the error codes by falses.
204
         */
205
        foreach ($result as $key => $status) {
206
            $result[$key] = $status === true;
207
        }
208
209
        return $result;
210
    }
211
212
    /**
213
     * {@inheritdoc}
214
     */
215
    public function add($key, $value, $expire = 0)
216
    {
217
        $key = $this->encode($key);
218
219
        return $this->client->add($key, $value, $expire);
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225
    public function replace($key, $value, $expire = 0)
226
    {
227
        $key = $this->encode($key);
228
229
        return $this->client->replace($key, $value, $expire);
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     */
235 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...
236
    {
237
        if (!is_float($token)) {
238
            return false;
239
        }
240
241
        $key = $this->encode($key);
242
243
        return $this->client->cas($token, $key, $value, $expire);
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     */
249 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...
250
    {
251
        if ($offset <= 0 || $initial < 0) {
252
            return false;
253
        }
254
255
        /*
256
         * Not doing \Memcached::increment because that one:
257
         * * needs \Memcached::OPT_BINARY_PROTOCOL == true
258
         * * is prone to errors after a flush ("merges" with pruned data) in at
259
         *   least some particular versions of Memcached
260
         */
261
        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 261 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::increment of type integer|boolean.
Loading history...
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     */
267 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...
268
    {
269
        if ($offset <= 0 || $initial < 0) {
270
            return false;
271
        }
272
273
        /*
274
         * Not doing \Memcached::decrement for the reasons described in:
275
         * @see increment()
276
         */
277
        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 277 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::decrement of type integer|boolean.
Loading history...
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     */
283
    public function touch($key, $expire)
284
    {
285
        /*
286
         * Since \Memcached has no reliable touch(), we might as well take an
287
         * easy approach where we can. If TTL is expired already, just delete
288
         * the key - this only needs 1 request.
289
         */
290 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...
291
            return $this->delete($key);
292
        }
293
294
        /*
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...
295
         * HHVM doesn't support touch.
296
         * @see http://docs.hhvm.com/manual/en/memcached.touch.php
297
         *
298
         * PHP does, but only with \Memcached::OPT_BINARY_PROTOCOL == true,
299
         * and even then, it appears to be buggy on particular versions of
300
         * Memcached.
301
         *
302
         * I'll just work around it!
303
         */
304
        $value = $this->get($key, $token);
305
306
        return $this->cas($token, $key, $value, $expire);
307
    }
308
309
    /**
310
     * {@inheritdoc}
311
     */
312
    public function flush()
313
    {
314
        return $this->client->flush();
315
    }
316
317
    /**
318
     * {@inheritdoc}
319
     */
320
    public function getCollection($name)
321
    {
322
        return new Collection($this, $name);
323
    }
324
325
    /**
326
     * Shared between increment/decrement: both have mostly the same logic
327
     * (decrement just increments a negative value), but need their validation
328
     * split up (increment won't accept negative values).
329
     *
330
     * @param string $key
331
     * @param int    $offset
332
     * @param int    $initial
333
     * @param int    $expire
334
     *
335
     * @return int|bool
336
     */
337 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...
338
    {
339
        $value = $this->get($key, $token);
340
        if ($value === false) {
341
            $success = $this->add($key, $initial, $expire);
342
343
            return $success ? $initial : false;
344
        }
345
346
        if (!is_numeric($value) || $value < 0) {
347
            return false;
348
        }
349
350
        $value += $offset;
351
        // value can never be lower than 0
352
        $value = max(0, $value);
353
        $key = $this->encode($key);
354
        $success = $this->client->cas($token, $key, $value, $expire);
355
356
        return $success ? $value : false;
357
    }
358
359
    /**
360
     * Encode a key for use on the wire inside the memcached protocol.
361
     *
362
     * We encode spaces and line breaks to avoid protocol errors. We encode
363
     * the other control characters for compatibility with libmemcached
364
     * verify_key. We leave other punctuation alone, to maximise backwards
365
     * compatibility.
366
     *
367
     * @see https://github.com/wikimedia/mediawiki/commit/be76d869#diff-75b7c03970b5e43de95ff95f5faa6ef1R100
368
     * @see https://github.com/wikimedia/mediawiki/blob/master/includes/libs/objectcache/MemcachedBagOStuff.php#L116
369
     *
370
     * @param string $key
371
     *
372
     * @return string
373
     *
374
     * @throws InvalidKey
375
     */
376
    protected function encode($key)
377
    {
378
        $regex = '/[^\x21\x22\x24\x26-\x39\x3b-\x7e]+/';
379
        $key = preg_replace_callback($regex, function ($match) {
380
            return rawurlencode($match[0]);
381
        }, $key);
382
383
        if (strlen($key) > 255) {
384
            throw new InvalidKey(
385
                "Invalid key: $key. Encoded Memcached keys can not exceed 255 chars."
386
            );
387
        }
388
389
        return $key;
390
    }
391
392
    /**
393
     * Decode a key encoded with encode().
394
     *
395
     * @param string $key
396
     *
397
     * @return string
398
     */
399
    protected function decode($key)
400
    {
401
        // 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...
402
        // (=the encoded versions for those encoded in encode)
403
        $regex = '/%(?!2[1246789]|3[0-9]|3[B-F]|[4-6][0-9A-F]|5[0-9A-E])[0-9A-Z]{2}/i';
404
405
        return preg_replace_callback($regex, function ($match) {
406
            return rawurldecode($match[0]);
407
        }, $key);
408
    }
409
}
410