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