Couchbase   F
last analyzed

Complexity

Total Complexity 73

Size/Duplication

Total Lines 443
Duplicated Lines 20.99 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
wmc 73
lcom 1
cbo 2
dl 93
loc 443
rs 2.56
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A get() 0 14 3
B getMulti() 0 27 6
A set() 0 15 3
B setMulti() 0 47 8
A delete() 0 10 2
A deleteMulti() 0 19 4
A add() 19 19 4
A replace() 19 19 4
A cas() 19 19 4
A increment() 8 8 3
A decrement() 8 8 3
A touch() 0 14 3
B flush() 0 43 5
A getCollection() 0 4 1
B doIncrement() 20 20 6
A serialize() 0 4 3
A unserialize() 0 6 2
A deleteIfExpired() 0 10 4
A assertServerHealhy() 0 9 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Couchbase often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Couchbase, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace MatthiasMullie\Scrapbook\Adapters;
4
5
use MatthiasMullie\Scrapbook\Adapters\Collections\Couchbase as Collection;
6
use MatthiasMullie\Scrapbook\Exception\ServerUnhealthy;
7
use MatthiasMullie\Scrapbook\KeyValueStore;
8
9
/**
10
 * Couchbase adapter. Basically just a wrapper over \CouchbaseBucket, but in an
11
 * exchangeable (KeyValueStore) interface.
12
 *
13
 * @see http://developer.couchbase.com/documentation/server/4.0/sdks/php-2.0/php-intro.html
14
 * @see http://docs.couchbase.com/sdk-api/couchbase-php-client-2.1.0/
15
 *
16
 * @author Matthias Mullie <[email protected]>
17
 * @copyright Copyright (c) 2014, Matthias Mullie. All rights reserved
18
 * @license LICENSE MIT
19
 */
20
class Couchbase implements KeyValueStore
21
{
22
    /**
23
     * @var \CouchbaseBucket
24
     */
25
    protected $client;
26
27
    /**
28
     * @param \CouchbaseBucket $client
29
     * @param bool             $assertServerHealthy
30
     *
31
     * @throws ServerUnhealthy
32
     */
33
    public function __construct(\CouchbaseBucket $client, $assertServerHealthy = true)
34
    {
35
        $this->client = $client;
36
37
        if ($assertServerHealthy) {
38
            $this->assertServerHealhy();
39
        }
40
    }
41
42
    /**
43
     * {@inheritdoc}
44
     */
45
    public function get($key, &$token = null)
46
    {
47
        try {
48
            $result = $this->client->get($key);
49
        } catch (\CouchbaseException $e) {
50
            $token = null;
51
52
            return false;
53
        }
54
55
        $token = $result->cas;
56
57
        return $result->error ? false : $this->unserialize($result->value);
58
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63
    public function getMulti(array $keys, array &$tokens = null)
64
    {
65
        $tokens = array();
66
        if (empty($keys)) {
67
            return array();
68
        }
69
70
        try {
71
            $results = $this->client->get($keys);
72
        } catch (\CouchbaseException $e) {
73
            return array();
74
        }
75
76
        $values = array();
77
        $tokens = array();
78
79
        foreach ($results as $key => $value) {
80
            if (!in_array($key, $keys) || $value->error) {
81
                continue;
82
            }
83
84
            $values[$key] = $this->unserialize($value->value);
85
            $tokens[$key] = $value->cas;
86
        }
87
88
        return $values;
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94
    public function set($key, $value, $expire = 0)
95
    {
96
        if ($this->deleteIfExpired($key, $expire)) {
97
            return true;
98
        }
99
100
        $value = $this->serialize($value);
101
        try {
102
            $result = $this->client->upsert($key, $value, array('expiry' => $expire));
103
        } catch (\CouchbaseException $e) {
104
            return false;
105
        }
106
107
        return !$result->error;
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113
    public function setMulti(array $items, $expire = 0)
114
    {
115
        if (empty($items)) {
116
            return array();
117
        }
118
119
        $keys = array_keys($items);
120
        if ($this->deleteIfExpired($keys, $expire)) {
121
            return array_fill_keys($keys, true);
122
        }
123
124
        // attempting to insert integer keys (e.g. '0' as key is automatically
125
        // cast to int, if it's an array key) fails with a segfault, so we'll
126
        // have to do those piecemeal
127
        $integers = array_filter(array_keys($items), 'is_int');
128
        if ($integers) {
0 ignored issues
show
Bug Best Practice introduced by Matthias Mullie
The expression $integers 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...
129
            $success = array();
130
            $integers = array_intersect_key($items, array_fill_keys($integers, null));
131
            foreach ($integers as $k => $v) {
132
                $success[$k] = $this->set((string) $k, $v, $expire);
133
            }
134
135
            $items = array_diff_key($items, $integers);
136
137
            return array_merge($success, $this->setMulti($items, $expire));
138
        }
139
140
        foreach ($items as $key => $value) {
141
            $items[$key] = array(
142
                'value' => $this->serialize($value),
143
                'expiry' => $expire,
144
            );
145
        }
146
147
        try {
148
            $results = $this->client->upsert($items);
149
        } catch (\CouchbaseException $e) {
150
            return array_fill_keys(array_keys($items), false);
151
        }
152
153
        $success = array();
154
        foreach ($results as $key => $result) {
155
            $success[$key] = !$result->error;
156
        }
157
158
        return $success;
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164
    public function delete($key)
165
    {
166
        try {
167
            $result = $this->client->remove($key);
168
        } catch (\CouchbaseException $e) {
169
            return false;
170
        }
171
172
        return !$result->error;
173
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178
    public function deleteMulti(array $keys)
179
    {
180
        if (empty($keys)) {
181
            return array();
182
        }
183
184
        try {
185
            $results = $this->client->remove($keys);
186
        } catch (\CouchbaseException $e) {
187
            return array_fill_keys($keys, false);
188
        }
189
190
        $success = array();
191
        foreach ($results as $key => $result) {
192
            $success[$key] = !$result->error;
193
        }
194
195
        return $success;
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 View Code Duplication
    public function add($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...
202
    {
203
        $value = $this->serialize($value);
204
        try {
205
            $result = $this->client->insert($key, $value, array('expiry' => $expire));
206
        } catch (\CouchbaseException $e) {
207
            return false;
208
        }
209
210
        $success = !$result->error;
211
212
        // Couchbase is imprecise in its expiration handling, so we can clean up
213
        // stuff that is already expired (assuming the `add` succeeded)
214
        if ($success && $this->deleteIfExpired($key, $expire)) {
215
            return true;
216
        }
217
218
        return $success;
219
    }
220
221
    /**
222
     * {@inheritdoc}
223
     */
224 View Code Duplication
    public function replace($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...
225
    {
226
        $value = $this->serialize($value);
227
        try {
228
            $result = $this->client->replace($key, $value, array('expiry' => $expire));
229
        } catch (\CouchbaseException $e) {
230
            return false;
231
        }
232
233
        $success = !$result->error;
234
235
        // Couchbase is imprecise in its expiration handling, so we can clean up
236
        // stuff that is already expired (assuming the `replace` succeeded)
237
        if ($success && $this->deleteIfExpired($key, $expire)) {
238
            return true;
239
        }
240
241
        return $success;
242
    }
243
244
    /**
245
     * {@inheritdoc}
246
     */
247 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...
248
    {
249
        $value = $this->serialize($value);
250
        try {
251
            $result = $this->client->replace($key, $value, array('expiry' => $expire, 'cas' => $token));
252
        } catch (\CouchbaseException $e) {
253
            return false;
254
        }
255
256
        $success = !$result->error;
257
258
        // Couchbase is imprecise in its expiration handling, so we can clean up
259
        // stuff that is already expired (assuming the `cas` succeeded)
260
        if ($success && $this->deleteIfExpired($key, $expire)) {
261
            return true;
262
        }
263
264
        return $success;
265
    }
266
267
    /**
268
     * {@inheritdoc}
269
     */
270 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...
271
    {
272
        if ($offset <= 0 || $initial < 0) {
273
            return false;
274
        }
275
276
        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 276 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::increment of type integer|boolean.
Loading history...
277
    }
278
279
    /**
280
     * {@inheritdoc}
281
     */
282 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...
283
    {
284
        if ($offset <= 0 || $initial < 0) {
285
            return false;
286
        }
287
288
        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 288 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::decrement of type integer|boolean.
Loading history...
289
    }
290
291
    /**
292
     * {@inheritdoc}
293
     */
294
    public function touch($key, $expire)
295
    {
296
        if ($this->deleteIfExpired($key, $expire)) {
297
            return true;
298
        }
299
300
        try {
301
            $result = $this->client->getAndTouch($key, $expire);
302
        } catch (\CouchbaseException $e) {
303
            return false;
304
        }
305
306
        return !$result->error;
307
    }
308
309
    /**
310
     * {@inheritdoc}
311
     */
312
    public function flush()
313
    {
314
        // depending on config & client version, flush may not be available
315
        try {
316
            /*
317
             * Flush wasn't always properly implemented[1] in the client, plus
318
             * it depends on server config[2] to be enabled. Return status has
319
             * been null in both success & failure cases.
320
             * Flush is a very pervasive function that's likely not called
321
             * lightly. Since it's probably more important to know whether or
322
             * not it succeeded, than having it execute as fast as possible, I'm
323
             * going to add some calls and test if flush succeeded.
324
             *
325
             * 1: https://forums.couchbase.com/t/php-flush-isnt-doing-anything/1886/8
326
             * 2: http://docs.couchbase.com/admin/admin/CLI/CBcli/cbcli-bucket-flush.html
327
             */
328
            $this->client->upsert('cb-flush-tester', '');
329
330
            $manager = $this->client->manager();
331
            if (method_exists($manager, 'flush')) {
332
                // ext-couchbase >= 2.0.6
333
                $manager->flush();
334
            } elseif (method_exists($this->client, 'flush')) {
335
                // ext-couchbase < 2.0.6
336
                $this->client->flush();
337
            } else {
338
                return false;
339
            }
340
        } catch (\CouchbaseException $e) {
341
            return false;
342
        }
343
344
        try {
345
            // cleanup in case flush didn't go through; but if it did, we won't
346
            // be able to remove it and know flush succeeded
347
            $result = $this->client->remove('cb-flush-tester');
348
349
            return (bool) $result->error;
350
        } catch (\CouchbaseException $e) {
351
            // exception: "The key does not exist on the server"
352
            return true;
353
        }
354
    }
355
356
    /**
357
     * {@inheritdoc}
358
     */
359
    public function getCollection($name)
360
    {
361
        return new Collection($this, $name);
362
    }
363
364
    /**
365
     * We could use `$this->client->counter()`, but it doesn't seem to respect
366
     * data types and stores the values as strings instead of integers.
367
     *
368
     * Shared between increment/decrement: both have mostly the same logic
369
     * (decrement just increments a negative value), but need their validation
370
     * split up (increment won't accept negative values).
371
     *
372
     * @param string $key
373
     * @param int    $offset
374
     * @param int    $initial
375
     * @param int    $expire
376
     *
377
     * @return int|bool
378
     */
379 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...
380
    {
381
        $value = $this->get($key, $token);
382
        if ($value === false) {
383
            $success = $this->add($key, $initial, $expire);
384
385
            return $success ? $initial : false;
386
        }
387
388
        if (!is_numeric($value) || $value < 0) {
389
            return false;
390
        }
391
392
        $value += $offset;
393
        // value can never be lower than 0
394
        $value = max(0, $value);
395
        $success = $this->cas($token, $key, $value, $expire);
396
397
        return $success ? $value : false;
398
    }
399
400
    /**
401
     * Couchbase doesn't properly remember the data type being stored:
402
     * arrays and objects are turned into stdClass instances.
403
     *
404
     * @param mixed $value
405
     *
406
     * @return string|mixed
407
     */
408
    protected function serialize($value)
409
    {
410
        return (is_array($value) || is_object($value)) ? serialize($value) : $value;
411
    }
412
413
    /**
414
     * Restore serialized data.
415
     *
416
     * @param mixed $value
417
     *
418
     * @return mixed|int|float
419
     */
420
    protected function unserialize($value)
421
    {
422
        $unserialized = @unserialize($value);
423
424
        return $unserialized === false ? $value : $unserialized;
425
    }
426
427
    /**
428
     * Couchbase seems to not timely purge items the way it should when
429
     * storing it with an expired timestamp, so we'll detect that and
430
     * delete it (instead of performing the already expired operation).
431
     *
432
     * @param string|string[] $key
433
     * @param int             $expire
434
     *
435
     * @return int TTL in seconds
436
     */
437
    protected function deleteIfExpired($key, $expire)
438
    {
439
        if ($expire < 0 || ($expire > 2592000 && $expire < time())) {
440
            $this->deleteMulti((array) $key);
441
442
            return true;
443
        }
444
445
        return false;
446
    }
447
448
    /**
449
     * Verify that the server is healthy.
450
     *
451
     * @throws ServerUnhealthy
452
     */
453
    protected function assertServerHealhy()
454
    {
455
        $info = $this->client->manager()->info();
456
        foreach ($info['nodes'] as $node) {
457
            if ($node['status'] !== 'healthy') {
458
                throw new ServerUnhealthy('Server isn\'t ready yet');
459
            }
460
        }
461
    }
462
}
463