Completed
Push — master ( 3597b9...9dc24f )
by Matthias
01:45
created

Couchbase::assertServerHealhy()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 3
eloc 5
nc 3
nop 0
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
        // Couchbase seems to not timely purge items the way it should when
97
        // storing it with an expired timestamp
98 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...
99
            $this->delete($key);
100
101
            return true;
102
        }
103
104
        $value = $this->serialize($value);
105
        try {
106
            $result = $this->client->upsert($key, $value, array('expiry' => $expire));
107
        } catch (\CouchbaseException $e) {
108
            return false;
109
        }
110
111
        return !$result->error;
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117
    public function setMulti(array $items, $expire = 0)
118
    {
119
        if (empty($items)) {
120
            return array();
121
        }
122
123
        // Couchbase seems to not timely purge items the way it should when
124
        // storing it with an expired timestamp
125 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...
126
            $keys = array_keys($items);
127
            $this->deleteMulti($keys);
128
129
            return array_fill_keys($keys, true);
130
        }
131
132
        // attempting to insert integer keys (e.g. '0' as key is automatically
133
        // cast to int, if it's an array key) fails with a segfault, so we'll
134
        // have to do those piecemeal
135
        $integers = array_filter(array_keys($items), 'is_int');
136
        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...
137
            $success = [];
138
            $integers = array_intersect_key($items, array_fill_keys($integers, null));
139
            foreach ($integers as $k => $v) {
140
                $success[$k] = $this->set((string) $k, $v, $expire);
141
            }
142
143
            $items = array_diff_key($items, $integers);
144
145
            return array_merge($success, $this->setMulti($items, $expire));
146
        }
147
148
        foreach ($items as $key => $value) {
149
            $items[$key] = array(
150
                'value' => $this->serialize($value),
151
                'expiry' => $expire,
152
            );
153
        }
154
155
        try {
156
            $results = $this->client->upsert($items);
157
        } catch (\CouchbaseException $e) {
158
            return array_fill_keys(array_keys($items), false);
159
        }
160
161
        $success = array();
162
        foreach ($results as $key => $result) {
163
            $success[$key] = !$result->error;
164
        }
165
166
        return $success;
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function delete($key)
173
    {
174
        try {
175
            $result = $this->client->remove($key);
176
        } catch (\CouchbaseException $e) {
177
            return false;
178
        }
179
180
        return !$result->error;
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     */
186
    public function deleteMulti(array $keys)
187
    {
188
        if (empty($keys)) {
189
            return array();
190
        }
191
192
        try {
193
            $results = $this->client->remove($keys);
194
        } catch (\CouchbaseException $e) {
195
            return array_fill_keys($keys, false);
196
        }
197
198
        $success = array();
199
        foreach ($results as $key => $result) {
200
            $success[$key] = !$result->error;
201
        }
202
203
        return $success;
204
    }
205
206
    /**
207
     * {@inheritdoc}
208
     */
209 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...
210
    {
211
        $value = $this->serialize($value);
212
        try {
213
            $result = $this->client->insert($key, $value, array('expiry' => $expire));
214
        } catch (\CouchbaseException $e) {
215
            return false;
216
        }
217
218
        return !$result->error;
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
        return !$result->error;
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239 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...
240
    {
241
        $value = $this->serialize($value);
242
        try {
243
            $result = $this->client->replace($key, $value, array('expiry' => $expire, 'cas' => $token));
244
        } catch (\CouchbaseException $e) {
245
            return false;
246
        }
247
248
        return !$result->error;
249
    }
250
251
    /**
252
     * {@inheritdoc}
253
     */
254 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...
255
    {
256
        if ($offset <= 0 || $initial < 0) {
257
            return false;
258
        }
259
260
        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 260 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::increment of type integer|boolean.
Loading history...
261
    }
262
263
    /**
264
     * {@inheritdoc}
265
     */
266 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...
267
    {
268
        if ($offset <= 0 || $initial < 0) {
269
            return false;
270
        }
271
272
        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 272 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::decrement of type integer|boolean.
Loading history...
273
    }
274
275
    /**
276
     * {@inheritdoc}
277
     */
278
    public function touch($key, $expire)
279
    {
280 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...
281
            return $this->delete($key);
282
        }
283
284
        try {
285
            $result = $this->client->getAndTouch($key, $expire);
286
        } catch (\CouchbaseException $e) {
287
            return false;
288
        }
289
290
        return !$result->error;
291
    }
292
293
    /**
294
     * {@inheritdoc}
295
     */
296
    public function flush()
297
    {
298
        // depending on config & client version, flush may not be available
299
        try {
300
            /*
301
             * Flush wasn't always properly implemented[1] in the client, plus
302
             * it depends on server config[2] to be enabled. Return status has
303
             * been null in both success & failure cases.
304
             * Flush is a very pervasive function that's likely not called
305
             * lightly. Since it's probably more important to know whether or
306
             * not it succeeded, than having it execute as fast as possible, I'm
307
             * going to add some calls and test if flush succeeded.
308
             *
309
             * 1: https://forums.couchbase.com/t/php-flush-isnt-doing-anything/1886/8
310
             * 2: http://docs.couchbase.com/admin/admin/CLI/CBcli/cbcli-bucket-flush.html
311
             */
312
            $this->client->upsert('cb-flush-tester', '');
313
314
            $manager = $this->client->manager();
315
            if (method_exists($manager, 'flush')) {
316
                // ext-couchbase >= 2.0.6
317
                $manager->flush();
318
            } elseif (method_exists($this->client, 'flush')) {
319
                // ext-couchbase < 2.0.6
320
                $this->client->flush();
321
            } else {
322
                return false;
323
            }
324
        } catch (\CouchbaseException $e) {
325
            return false;
326
        }
327
328
        try {
329
            // cleanup in case flush didn't go through; but if it did, we won't
330
            // be able to remove it and know flush succeeded
331
            $result = $this->client->remove('cb-flush-tester');
332
333
            return (bool) $result->error;
334
        } catch (\CouchbaseException $e) {
335
            // exception: "The key does not exist on the server"
336
            return true;
337
        }
338
    }
339
340
    /**
341
     * {@inheritdoc}
342
     */
343
    public function getCollection($name)
344
    {
345
        return new Collection($this, $name);
346
    }
347
348
    /**
349
     * We could use `$this->client->counter()`, but it doesn't seem to respect
350
     * data types and stores the values as strings instead of integers.
351
     *
352
     * Shared between increment/decrement: both have mostly the same logic
353
     * (decrement just increments a negative value), but need their validation
354
     * split up (increment won't accept negative values).
355
     *
356
     * @param string $key
357
     * @param int    $offset
358
     * @param int    $initial
359
     * @param int    $expire
360
     *
361
     * @return int|bool
362
     */
363 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...
364
    {
365
        $value = $this->get($key, $token);
366
        if ($value === false) {
367
            $success = $this->add($key, $initial, $expire);
368
369
            return $success ? $initial : false;
370
        }
371
372
        if (!is_numeric($value) || $value < 0) {
373
            return false;
374
        }
375
376
        $value += $offset;
377
        // value can never be lower than 0
378
        $value = max(0, $value);
379
        $success = $this->cas($token, $key, $value, $expire);
380
381
        return $success ? $value : false;
382
    }
383
384
    /**
385
     * Couchbase doesn't properly remember the data type being stored:
386
     * arrays and objects are turned into stdClass instances.
387
     *
388
     * @param mixed $value
389
     *
390
     * @return string|mixed
391
     */
392
    protected function serialize($value)
393
    {
394
        return (is_array($value) || is_object($value)) ? serialize($value) : $value;
395
    }
396
397
    /**
398
     * Restore serialized data.
399
     *
400
     * @param mixed $value
401
     *
402
     * @return mixed|int|float
403
     */
404
    protected function unserialize($value)
405
    {
406
        $unserialized = @unserialize($value);
407
408
        return $unserialized === false ? $value : $unserialized;
409
    }
410
411
    /**
412
     * Verify that the server is healthy.
413
     *
414
     * @throws ServerUnhealthy
415
     */
416
    protected function assertServerHealhy()
417
    {
418
        $info = $this->client->manager()->info();
419
        foreach ($info['nodes'] as $node) {
420
            if ($node['status'] !== 'healthy') {
421
                throw new ServerUnhealthy('Server isn\'t ready yet');
422
            }
423
        }
424
    }
425
}
426