Completed
Pull Request — master (#272)
by
unknown
32:51
created

CouchbaseBucketCache   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 201
Duplicated Lines 0 %

Test Coverage

Coverage 85.29%

Importance

Changes 0
Metric Value
dl 0
loc 201
ccs 58
cts 68
cp 0.8529
rs 10
c 0
b 0
f 0
wmc 24

11 Methods

Rating   Name   Duplication   Size   Complexity  
A doFlush() 0 17 2
A doDelete() 0 15 3
A doFetch() 0 15 4
A getBucket() 0 3 1
A doContains() 0 15 3
A normalizeExpiry() 0 7 2
A normalizeKey() 0 9 2
A doSave() 0 21 3
A __construct() 0 5 2
A setBucket() 0 3 1
A doGetStats() 0 14 1
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
class CouchbaseBucketCache extends CacheProvider
22
{
23
    public const MINIMUM_VERSION = '2.3.0';
24
25
    public const KEY_NOT_FOUND = 13;
26
27
    public const MAX_KEY_LENGTH = 250;
28
29
    public const THIRTY_DAYS_IN_SECONDS = 2592000;
30
31
    /** @var Bucket */
32
    private $bucket;
33
34 78
    public function __construct()
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
42 78
    /**
43
     * Sets the Couchbase Bucket to use
44
     *
45
     * @param \Couchbase\Bucket $bucket
0 ignored issues
show
introduced by
Class \Couchbase\Bucket should not be referenced via a fully qualified name, but via a use statement.
Loading history...
46
     */
47 76
    public function setBucket(Bucket $bucket): void
0 ignored issues
show
introduced by
There must be exactly 1 whitespace between closing parenthesis and return type colon.
Loading history...
48
    {
49 76
        $this->bucket = $bucket;
50
    }
51
52 76
    /**
53 66
     * Gets the Couchbase bucket used by the cache
54 66
     *
55
     * @return \Couchbase\Bucket
0 ignored issues
show
introduced by
Class \Couchbase\Bucket should not be referenced via a fully qualified name, but via a use statement.
Loading history...
56
     */
57 73
    public function getBucket(): Bucket
0 ignored issues
show
introduced by
There must be exactly 1 whitespace between closing parenthesis and return type colon.
Loading history...
58 73
    {
59
        return $this->bucket;
60
    }
61
62
    /**
63
     * {@inheritdoc}
64
     */
65
    protected function doFetch($id)
66
    {
67 73
        $id = $this->normalizeKey($id);
68
69 73
        try {
70
            $document = $this->bucket->get($id);
71
        } catch (Exception $e) {
72 73
            return false;
73 53
        }
74 53
75
        if ($document instanceof Document && $document->value !== false) {
76
            return unserialize($document->value);
77 69
        }
78 69
79
        return false;
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85
    protected function doContains($id)
86
    {
87 74
        $id = $this->normalizeKey($id);
88
89 74
        try {
90
            $document = $this->bucket->get($id);
91 74
        } catch (Exception $e) {
92
            return false;
93
        }
94 74
95
        if ($document instanceof Document) {
96 74
            return ! $document->error;
97 74
        }
98
99
        return false;
100
    }
101
102
    /**
103 74
     * {@inheritdoc}
104 74
     */
105
    protected function doSave($id, $data, $lifeTime = 0)
106
    {
107
        $id = $this->normalizeKey($id);
108
109
        $lifeTime = $this->normalizeExpiry($lifeTime);
110
111
        try {
112
            $encoded = serialize($data);
113 45
114
            $document = $this->bucket->upsert($id, $encoded, [
115 45
                'expiry' => (int) $lifeTime,
116
            ]);
117
        } catch (Exception $e) {
118 45
            return false;
119 3
        }
120 3
121
        if ($document instanceof Document) {
122
            return ! $document->error;
123 43
        }
124 43
125
        return false;
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    protected function doDelete($id)
132
    {
133 2
        $id = $this->normalizeKey($id);
134
135 2
        try {
136
            $document = $this->bucket->remove($id);
137
        } catch (Exception $e) {
138
            return $e->getCode() === self::KEY_NOT_FOUND;
139 2
        }
140
141 2
        if ($document instanceof Document) {
142
            return ! $document->error;
143 2
        }
144
145
        return false;
146
    }
147
148
    /**
149 2
     * {@inheritdoc}
150
     */
151
    protected function doFlush()
152
    {
153
        $manager = $this->bucket->manager();
154
155 1
        // Flush does not return with success or failure, and must be enabled per bucket on the server.
156
        // Store a marker item so that we will know if it was successful.
157 1
        $this->doSave(__METHOD__, true, 60);
158 1
159 1
        $manager->flush();
160 1
161 1
        if ($this->doContains(__METHOD__)) {
162
            $this->doDelete(__METHOD__);
163
164 1
            return false;
165 1
        }
166 1
167 1
        return true;
168 1
    }
169
170
    /**
171
     * {@inheritdoc}
172 76
     */
173
    protected function doGetStats()
174 76
    {
175
        $manager          = $this->bucket->manager();
176 76
        $stats            = $manager->info();
177
        $nodes            = $stats['nodes'];
178
        $node             = $nodes[0];
179
        $interestingStats = $node['interestingStats'];
180 76
181
        return [
182
            Cache::STATS_HITS   => $interestingStats['get_hits'],
183
            Cache::STATS_MISSES => $interestingStats['cmd_get'] - $interestingStats['get_hits'],
184
            Cache::STATS_UPTIME => $node['uptime'],
185
            Cache::STATS_MEMORY_USAGE     => $interestingStats['mem_used'],
186
            Cache::STATS_MEMORY_AVAILABLE => $node['memoryFree'],
187 74
        ];
188
    }
189 74
190 1
    /**
191
     * Ensure key is less than 250 bytes in length
192
     * @see https://developer.couchbase.com/documentation/server/current/clustersetup/server-setup.html under "Limits"
193 73
     *
194
     * @param string $id
195
     * @return string
196
     */
197
    protected function normalizeKey(string $id) : string
198
    {
199
        $normalized = substr($id, 0, self::MAX_KEY_LENGTH);
200
201
        if ($normalized === false) {
202
            return $id;
203
        }
204
205
        return $normalized;
206
    }
207
208
    /**
209
     * Expiry treated as a unix timestamp instead of an offset if expiry is greater than 30 days.
210
     * @src https://developer.couchbase.com/documentation/server/4.1/developer-guide/expiry.html
211
     *
212
     * @param int $expiry
213
     * @return int
214
     */
215
    protected function normalizeExpiry(int $expiry) : int
216
    {
217
        if ($expiry > self::THIRTY_DAYS_IN_SECONDS) {
218
            return time() + $expiry;
219
        }
220
221
        return $expiry;
222
    }
223
}
224