Completed
Push — master ( 45ac19...9eb216 )
by Matthias
02:34
created

Memcached::setMulti()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 22
Code Lines 12

Duplication

Lines 6
Ratio 27.27 %

Importance

Changes 0
Metric Value
dl 6
loc 22
rs 8.6737
c 0
b 0
f 0
cc 5
eloc 12
nc 3
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
        $keys = array_map(array($this, 'encode'), array_keys($items));
120
        $items = array_combine($keys, $items);
121
        $success = $this->client->setMulti($items, $expire);
122
        $keys = array_map(array($this, 'decode'), array_keys($items));
123
124
        return array_fill_keys($keys, $success);
125
    }
126
127
    /**
128
     * {@inheritdoc}
129
     */
130
    public function delete($key)
131
    {
132
        $key = $this->encode($key);
133
134
        return $this->client->delete($key);
135
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140
    public function deleteMulti(array $keys)
141
    {
142
        if (empty($keys)) {
143
            return array();
144
        }
145
146
        if (!method_exists($this->client, 'deleteMulti')) {
147
            /*
148
             * HHVM didn't always support deleteMulti, so I'll hack around it by
149
             * setting all items expired.
150
             * I could also delete() all items one by one, but that would
151
             * probably take more network requests (this version always takes 2)
152
             *
153
             * @see http://docs.hhvm.com/manual/en/memcached.deletemulti.php
154
             */
155
            $values = $this->getMulti($keys);
156
157
            $keys = array_map(array($this, 'encode'), array_keys($values));
158
            $this->client->setMulti(array_fill_keys($keys, ''), time() - 1);
159
160
            $return = array();
161
            foreach ($keys as $key) {
162
                $key = $this->decode($key);
163
                $return[$key] = array_key_exists($key, $values);
164
            }
165
166
            return $return;
167
        }
168
169
        $keys = array_map(array($this, 'encode'), $keys);
170
        $result = (array) $this->client->deleteMulti($keys);
171
        $keys = array_map(array($this, 'decode'), array_keys($result));
172
        $result = array_combine($keys, $result);
173
174
        /*
175
         * Contrary to docs (http://php.net/manual/en/memcached.deletemulti.php)
176
         * deleteMulti returns an array of [key => true] (for successfully
177
         * deleted values) and [key => error code] (for failures)
178
         * Pretty good because I want an array of true/false, so I'll just have
179
         * to replace the error codes by falses.
180
         */
181
        foreach ($result as $key => $status) {
182
            $result[$key] = $status === true;
183
        }
184
185
        return $result;
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191
    public function add($key, $value, $expire = 0)
192
    {
193
        $key = $this->encode($key);
194
195
        return $this->client->add($key, $value, $expire);
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201
    public function replace($key, $value, $expire = 0)
202
    {
203
        $key = $this->encode($key);
204
205
        return $this->client->replace($key, $value, $expire);
206
    }
207
208
    /**
209
     * {@inheritdoc}
210
     */
211 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...
212
    {
213
        if (!is_float($token)) {
214
            return false;
215
        }
216
217
        $key = $this->encode($key);
218
219
        return $this->client->cas($token, $key, $value, $expire);
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225 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...
226
    {
227
        if ($offset <= 0 || $initial < 0) {
228
            return false;
229
        }
230
231
        /*
232
         * Not doing \Memcached::increment because that one:
233
         * * needs \Memcached::OPT_BINARY_PROTOCOL == true
234
         * * is prone to errors after a flush ("merges" with pruned data) in at
235
         *   least some particular versions of Memcached
236
         */
237
        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 237 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::increment of type integer|boolean.
Loading history...
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243 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...
244
    {
245
        if ($offset <= 0 || $initial < 0) {
246
            return false;
247
        }
248
249
        /*
250
         * Not doing \Memcached::decrement for the reasons described in:
251
         * @see increment()
252
         */
253
        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 253 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::decrement of type integer|boolean.
Loading history...
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259
    public function touch($key, $expire)
260
    {
261
        /*
262
         * Since \Memcached has no reliable touch(), we might as well take an
263
         * easy approach where we can. If TTL is expired already, just delete
264
         * the key - this only needs 1 request.
265
         */
266 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...
267
            return $this->delete($key);
268
        }
269
270
        /*
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...
271
         * HHVM doesn't support touch.
272
         * @see http://docs.hhvm.com/manual/en/memcached.touch.php
273
         *
274
         * PHP does, but only with \Memcached::OPT_BINARY_PROTOCOL == true,
275
         * and even then, it appears to be buggy on particular versions of
276
         * Memcached.
277
         *
278
         * I'll just work around it!
279
         */
280
        $value = $this->get($key, $token);
281
282
        return $this->cas($token, $key, $value, $expire);
283
    }
284
285
    /**
286
     * {@inheritdoc}
287
     */
288
    public function flush()
289
    {
290
        return $this->client->flush();
291
    }
292
293
    /**
294
     * {@inheritdoc}
295
     */
296
    public function getCollection($name)
297
    {
298
        return new Collection($this, $name);
299
    }
300
301
    /**
302
     * Shared between increment/decrement: both have mostly the same logic
303
     * (decrement just increments a negative value), but need their validation
304
     * split up (increment won't accept negative values).
305
     *
306
     * @param string $key
307
     * @param int    $offset
308
     * @param int    $initial
309
     * @param int    $expire
310
     *
311
     * @return int|bool
312
     */
313 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...
314
    {
315
        $value = $this->get($key, $token);
316
        if ($value === false) {
317
            $success = $this->add($key, $initial, $expire);
318
319
            return $success ? $initial : false;
320
        }
321
322
        if (!is_numeric($value) || $value < 0) {
323
            return false;
324
        }
325
326
        $value += $offset;
327
        // value can never be lower than 0
328
        $value = max(0, $value);
329
        $key = $this->encode($key);
330
        $success = $this->client->cas($token, $key, $value, $expire);
331
332
        return $success ? $value : false;
333
    }
334
335
    /**
336
     * Encode a key for use on the wire inside the memcached protocol.
337
     *
338
     * We encode spaces and line breaks to avoid protocol errors. We encode
339
     * the other control characters for compatibility with libmemcached
340
     * verify_key. We leave other punctuation alone, to maximise backwards
341
     * compatibility.
342
     *
343
     * @see https://github.com/wikimedia/mediawiki/commit/be76d869#diff-75b7c03970b5e43de95ff95f5faa6ef1R100
344
     * @see https://github.com/wikimedia/mediawiki/blob/master/includes/libs/objectcache/MemcachedBagOStuff.php#L116
345
     *
346
     * @param string $key
347
     *
348
     * @return string
349
     *
350
     * @throws InvalidKey
351
     */
352
    protected function encode($key)
353
    {
354
        $regex = '/[^\x21\x22\x24\x26-\x39\x3b-\x7e]+/';
355
        $key = preg_replace_callback($regex, function ($match) {
356
            return rawurlencode($match[0]);
357
        }, $key);
358
359
        if (strlen($key) > 255) {
360
            throw new InvalidKey(
361
                "Invalid key: $key. Encoded Memcached keys can not exceed 255 chars."
362
            );
363
        }
364
365
        return $key;
366
    }
367
368
    /**
369
     * Decode a key encoded with encode().
370
     *
371
     * @param string $key
372
     *
373
     * @return string
374
     */
375
    protected function decode($key)
376
    {
377
        // 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...
378
        // (=the encoded versions for those encoded in encode)
379
        $regex = '/%(?!2[1246789]|3[0-9]|3[B-F]|[4-6][0-9A-F]|5[0-9A-E])[0-9A-Z]{2}/i';
380
381
        return preg_replace_callback($regex, function ($match) {
382
            return rawurldecode($match[0]);
383
        }, $key);
384
    }
385
}
386