Passed
Pull Request — master (#3)
by Alexander
01:15
created

Memcached::validateKey()   B

Complexity

Conditions 10
Paths 6

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
cc 10
dl 0
loc 14
rs 7.6666
c 0
b 0
f 0
eloc 8
nc 6
nop 3
ccs 0
cts 14
cp 0
crap 110

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
3
namespace Yiisoft\Cache\Memcached;
4
5
use DateInterval;
6
use DateTime;
7
use Psr\SimpleCache\CacheInterface;
8
9
/**
10
 * Memcached implements a cache application component based on [memcached](http://pecl.php.net/package/memcached) PECL
11
 * extension.
12
 *
13
 * Memcached can be configured with a list of memcached servers passed to the constructor.
14
 * By default, Memcached assumes there is a memcached server running on localhost at port 11211.
15
 *
16
 * See {@see \Psr\SimpleCache\CacheInterface} for common cache operations that MemCached supports.
17
 *
18
 * Note, there is no security measure to protected data in memcached.
19
 * All data in memcached can be accessed by any process running in the system.
20
 */
21
final class Memcached implements CacheInterface
22
{
23
    public const DEFAULT_SERVER_HOST = '127.0.0.1';
24
    public const DEFAULT_SERVER_PORT = 11211;
25
    public const DEFAULT_SERVER_WEIGHT = 1;
26
    private const EXPIRATION_INFINITY = 0;
27
    private const EXPIRATION_EXPIRED = -1;
28
29
    /**
30
     * @var \Memcached the Memcached instance
31
     */
32
    private $cache;
33
34
    /**
35
     * @var string an ID that identifies a Memcached instance.
36
     * By default the Memcached instances are destroyed at the end of the request. To create an instance that
37
     * persists between requests, you may specify a unique ID for the instance. All instances created with the
38
     * same ID will share the same connection.
39
     * @see https://www.php.net/manual/en/memcached.construct.php
40
     */
41
    private $persistentId;
42
43
    /**
44
     * @param string $persistentId By default the Memcached instances are destroyed at the end of the request. To create an
45
     * instance that persists between requests, use persistent_id to specify a unique ID for the instance. All instances
46
     * created with the same persistent_id will share the same connection.
47
     * @param array $servers list of memcached servers that will be added to the server pool
48
     * @see https://www.php.net/manual/en/memcached.construct.php
49
     * @see https://www.php.net/manual/en/memcached.addservers.php
50
     */
51
    public function __construct(string $persistentId = '', array $servers = [])
52
    {
53
        $this->validateServers($servers);
54
        $this->persistentId = $persistentId;
55
        $this->initCache();
56
        $this->initServers($servers);
57
    }
58
59
    public function get($key, $default = null)
60
    {
61
        $this->validateKey($key);
62
        $value = $this->cache->get($key);
63
64
        if ($this->cache->getResultCode() === \Memcached::RES_SUCCESS) {
65
            return $value;
66
        }
67
68
        return $default;
69
    }
70
71
    public function set($key, $value, $ttl = null): bool
72
    {
73
        $this->validateKey($key);
74
        $expiration = $this->ttlToExpiration($ttl);
75
        if ($expiration < 0) {
76
            return $this->delete($key);
77
        }
78
        return $this->cache->set($key, $value, $expiration);
79
    }
80
81
    public function delete($key): bool
82
    {
83
        $this->validateKey($key);
84
        return $this->cache->delete($key);
85
    }
86
87
    public function clear(): bool
88
    {
89
        return $this->cache->flush();
90
    }
91
92
    public function getMultiple($keys, $default = null): iterable
93
    {
94
        $this->validateKey($keys, true);
95
        $valuesFromCache = $this->cache->getMulti($this->iterableToArray($keys));
96
        $values = array_fill_keys($this->iterableToArray($keys), $default);
97
        foreach ($values as $key => $value) {
98
            $values[$key] = $valuesFromCache[$key] ?? $value;
99
        }
100
101
        return $values;
102
    }
103
104
    public function setMultiple($values, $ttl = null): bool
105
    {
106
        $this->validateKey($values, true, true);
107
        $expiration = $this->ttlToExpiration($ttl);
108
        return $this->cache->setMulti($this->iterableToArray($values), $expiration);
109
    }
110
111
    public function deleteMultiple($keys): bool
112
    {
113
        $this->validateKey($keys, true);
114
        foreach ($this->cache->deleteMulti($this->iterableToArray($keys)) as $result) {
115
            if ($result === false) {
116
                return false;
117
            }
118
        }
119
        return true;
120
    }
121
122
    public function has($key): bool
123
    {
124
        $this->validateKey($key);
125
        $this->cache->get($key);
126
        return $this->cache->getResultCode() === \Memcached::RES_SUCCESS;
127
    }
128
129
    /**
130
     * Returns underlying \Memcached instance
131
     * @return \Memcached
132
     */
133
    public function getCache(): \Memcached
134
    {
135
        return $this->cache;
136
    }
137
138
    /**
139
     * Inits Memcached instance
140
     */
141
    private function initCache(): void
142
    {
143
        $this->cache = new \Memcached($this->persistentId);
144
    }
145
146
    /**
147
     * Converts TTL to expiration
148
     * @param int|DateInterval|null $ttl
149
     * @return int
150
     */
151
    private function ttlToExpiration($ttl): int
152
    {
153
        $ttl = $this->normalizeTtl($ttl);
154
155
        if ($ttl === null) {
156
            $expiration = static::EXPIRATION_INFINITY;
157
        } elseif ($ttl <= 0) {
158
            $expiration = static::EXPIRATION_EXPIRED;
159
        } else {
160
            $expiration = $ttl + time();
161
        }
162
163
        return $expiration;
164
    }
165
166
    /**
167
     * @noinspection PhpDocMissingThrowsInspection DateTime won't throw exception because constant string is passed as time
168
     *
169
     * Normalizes cache TTL handling strings and {@see DateInterval} objects.
170
     * @param int|string|DateInterval|null $ttl raw TTL.
171
     * @return int|null TTL value as UNIX timestamp or null meaning infinity
172
     */
173
    private function normalizeTtl($ttl): ?int
174
    {
175
        if ($ttl instanceof DateInterval) {
176
            return (new DateTime('@0'))->add($ttl)->getTimestamp();
177
        }
178
179
        if (is_string($ttl)) {
180
            return (int)$ttl;
181
        }
182
183
        return $ttl;
184
    }
185
186
    /**
187
     * Converts iterable to array
188
     * @param iterable $iterable
189
     * @return array
190
     */
191
    private function iterableToArray(iterable $iterable): array
192
    {
193
        return $iterable instanceof \Traversable ? iterator_to_array($iterable) : (array)$iterable;
194
    }
195
196
    /**
197
     * @param array $servers
198
     */
199
    private function initServers(array $servers): void
200
    {
201
        if ($servers === []) {
202
            $servers = [
203
                [self::DEFAULT_SERVER_HOST, self::DEFAULT_SERVER_PORT, self::DEFAULT_SERVER_WEIGHT],
204
            ];
205
        }
206
207
        if ($this->persistentId !== '') {
208
            $servers = $this->getNewServers($servers);
209
        }
210
211
        $success = $this->cache->addServers($servers);
212
213
        if (!$success) {
214
            throw new CacheException('An error occurred while adding servers to the server pool.');
215
        }
216
    }
217
218
    /**
219
     * Returns the list of the servers that are not in the pool.
220
     * @param array $servers
221
     * @return array
222
     */
223
    private function getNewServers(array $servers): array
224
    {
225
        $existingServers = [];
226
        foreach ($this->cache->getServerList() as $existingServer) {
227
            $existingServers[$existingServer['host'] . ':' . $existingServer['port']] = true;
228
        }
229
230
        $newServers = [];
231
        foreach ($servers as $server) {
232
            $serverAddress = $server[0] . ':' . $server[1];
233
            if (!array_key_exists($serverAddress, $existingServers)) {
234
                $newServers[] = $server;
235
            }
236
        }
237
238
        return $newServers;
239
    }
240
241
    /**
242
     * Validates servers format
243
     * @param array $servers
244
     */
245
    private function validateServers(array $servers): void
246
    {
247
        foreach ($servers as $server) {
248
            if (!is_array($server) || !isset($server[0], $server[1])) {
249
                throw new CacheException('Each entry in servers parameter is supposed to be an array containing hostname, port, and, optionally, weight of the server.');
250
            }
251
        }
252
    }
253
254
    /**
255
     * Checks whether key is a legal value or not
256
     * @param mixed $key Key or array of keys ([key1, key2] or [key1 => val1, key2 => val2]) to be validated
257
     * @param bool $multiple Set to true if $key is an array of the following format [key1, key2]
258
     * @param bool $withValues Set to true if $key is an array of the following format [key1 => val1, key2 => val2]
259
     */
260
    private function validateKey($key, $multiple = false, $withValues = false): void
261
    {
262
        if ($multiple && !is_iterable($key)) {
263
            throw new InvalidArgumentException('Invalid $key value.');
264
        }
265
        if ($multiple && !$withValues) {
266
            foreach ($key as $item) {
267
                if (!\is_string($item) && !\is_int($item)) {
268
                    throw new InvalidArgumentException('Invalid $key value.');
269
                }
270
            }
271
        }
272
        if (!$multiple && !\is_string($key)) {
273
            throw new InvalidArgumentException('Invalid $key value.');
274
        }
275
    }
276
}
277