Completed
Push — master ( e1f38a...73ba45 )
by Marco
10s
created

CouchbaseBucketCache::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 8
ccs 3
cts 4
cp 0.75
crap 2.0625
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\Common\Cache;
6
7
use Couchbase\Bucket;
8
use Couchbase\Document;
9
use Couchbase\Exception;
10
use function phpversion;
11
use function serialize;
12
use function sprintf;
13
use function substr;
14
use function time;
15
use function unserialize;
16
use function version_compare;
17
18
/**
19
 * Couchbase ^2.3.0 cache provider.
20
 */
21
final class CouchbaseBucketCache extends CacheProvider
22
{
23
    private const MINIMUM_VERSION = '2.3.0';
24
25
    private const KEY_NOT_FOUND = 13;
26
27
    private const MAX_KEY_LENGTH = 250;
28
29
    private const THIRTY_DAYS_IN_SECONDS = 2592000;
30
31
    /** @var Bucket */
32
    private $bucket;
33
34 78
    public function __construct(Bucket $bucket)
35
    {
36 78
        if (version_compare(phpversion('couchbase'), self::MINIMUM_VERSION) < 0) {
37
            // Manager is required to flush cache and pull stats.
38
            throw new \RuntimeException(sprintf('ext-couchbase:^%s is required.', self::MINIMUM_VERSION));
39
        }
40
41 78
        $this->bucket = $bucket;
42 78
    }
43
44
    /**
45
     * {@inheritdoc}
46
     */
47 76
    protected function doFetch($id)
48
    {
49 76
        $id = $this->normalizeKey($id);
50
51
        try {
52 76
            $document = $this->bucket->get($id);
53 66
        } catch (Exception $e) {
54 66
            return false;
55
        }
56
57 73
        if ($document instanceof Document && $document->value !== false) {
58 73
            return unserialize($document->value);
59
        }
60
61
        return false;
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67 73
    protected function doContains($id)
68
    {
69 73
        $id = $this->normalizeKey($id);
70
71
        try {
72 73
            $document = $this->bucket->get($id);
73 53
        } catch (Exception $e) {
74 53
            return false;
75
        }
76
77 69
        if ($document instanceof Document) {
78 69
            return ! $document->error;
79
        }
80
81
        return false;
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87 74
    protected function doSave($id, $data, $lifeTime = 0)
88
    {
89 74
        $id = $this->normalizeKey($id);
90
91 74
        $lifeTime = $this->normalizeExpiry($lifeTime);
92
93
        try {
94 74
            $encoded = serialize($data);
95
96 74
            $document = $this->bucket->upsert($id, $encoded, [
97 74
                'expiry' => (int) $lifeTime,
98
            ]);
99
        } catch (Exception $e) {
100
            return false;
101
        }
102
103 74
        if ($document instanceof Document) {
104 74
            return ! $document->error;
105
        }
106
107
        return false;
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 45
    protected function doDelete($id)
114
    {
115 45
        $id = $this->normalizeKey($id);
116
117
        try {
118 45
            $document = $this->bucket->remove($id);
119 3
        } catch (Exception $e) {
120 3
            return $e->getCode() === self::KEY_NOT_FOUND;
121
        }
122
123 43
        if ($document instanceof Document) {
124 43
            return ! $document->error;
125
        }
126
127
        return false;
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133 2
    protected function doFlush()
134
    {
135 2
        $manager = $this->bucket->manager();
136
137
        // Flush does not return with success or failure, and must be enabled per bucket on the server.
138
        // Store a marker item so that we will know if it was successful.
139 2
        $this->doSave(__METHOD__, true, 60);
140
141 2
        $manager->flush();
142
143 2
        if ($this->doContains(__METHOD__)) {
144
            $this->doDelete(__METHOD__);
145
146
            return false;
147
        }
148
149 2
        return true;
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155 1
    protected function doGetStats()
156
    {
157 1
        $manager          = $this->bucket->manager();
158 1
        $stats            = $manager->info();
159 1
        $nodes            = $stats['nodes'];
160 1
        $node             = $nodes[0];
161 1
        $interestingStats = $node['interestingStats'];
162
163
        return [
164 1
            Cache::STATS_HITS   => $interestingStats['get_hits'],
165 1
            Cache::STATS_MISSES => $interestingStats['cmd_get'] - $interestingStats['get_hits'],
166 1
            Cache::STATS_UPTIME => $node['uptime'],
167 1
            Cache::STATS_MEMORY_USAGE     => $interestingStats['mem_used'],
168 1
            Cache::STATS_MEMORY_AVAILABLE => $node['memoryFree'],
169
        ];
170
    }
171
172 76
    private function normalizeKey(string $id) : string
173
    {
174 76
        $normalized = substr($id, 0, self::MAX_KEY_LENGTH);
175
176 76
        if ($normalized === false) {
177
            return $id;
178
        }
179
180 76
        return $normalized;
181
    }
182
183
    /**
184
     * Expiry treated as a unix timestamp instead of an offset if expiry is greater than 30 days.
185
     * @src https://developer.couchbase.com/documentation/server/4.1/developer-guide/expiry.html
186
     */
187 74
    private function normalizeExpiry(int $expiry) : int
188
    {
189 74
        if ($expiry > self::THIRTY_DAYS_IN_SECONDS) {
190 1
            return time() + $expiry;
191
        }
192
193 73
        return $expiry;
194
    }
195
}
196