Passed
Push — master ( 4fbdae...9f873b )
by Alexander
02:54
created

Memcached::validateKeysOfValues()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 0
cts 4
cp 0
crap 2
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
        $keys = $this->iterableToArray($keys);
95
        $this->validateKeys($keys);
96
        $valuesFromCache = $this->cache->getMulti($keys);
97
        $values = array_fill_keys($keys, $default);
98
        foreach ($values as $key => $value) {
99
            $values[$key] = $valuesFromCache[$key] ?? $value;
100
        }
101
102
        return $values;
103
    }
104
105
    public function setMultiple($values, $ttl = null): bool
106
    {
107
        $values = $this->iterableToArray($values);
108
        $this->validateKeysOfValues($values);
109
        $expiration = $this->ttlToExpiration($ttl);
110
        return $this->cache->setMulti($values, $expiration);
111
    }
112
113
    public function deleteMultiple($keys): bool
114
    {
115
        $keys = $this->iterableToArray($keys);
116
        $this->validateKeys($keys);
117
        foreach ($this->cache->deleteMulti($keys) as $result) {
118
            if ($result === false) {
119
                return false;
120
            }
121
        }
122
        return true;
123
    }
124
125
    public function has($key): bool
126
    {
127
        $this->validateKey($key);
128
        $this->cache->get($key);
129
        return $this->cache->getResultCode() === \Memcached::RES_SUCCESS;
130
    }
131
132
    /**
133
     * Returns underlying \Memcached instance
134
     * @return \Memcached
135
     */
136
    public function getCache(): \Memcached
137
    {
138
        return $this->cache;
139
    }
140
141
    /**
142
     * Inits Memcached instance
143
     */
144
    private function initCache(): void
145
    {
146
        $this->cache = new \Memcached($this->persistentId);
147
    }
148
149
    /**
150
     * Converts TTL to expiration
151
     * @param int|DateInterval|null $ttl
152
     * @return int
153
     */
154
    private function ttlToExpiration($ttl): int
155
    {
156
        $ttl = $this->normalizeTtl($ttl);
157
158
        if ($ttl === null) {
159
            $expiration = static::EXPIRATION_INFINITY;
160
        } elseif ($ttl <= 0) {
161
            $expiration = static::EXPIRATION_EXPIRED;
162
        } else {
163
            $expiration = $ttl + time();
164
        }
165
166
        return $expiration;
167
    }
168
169
    /**
170
     * @noinspection PhpDocMissingThrowsInspection DateTime won't throw exception because constant string is passed as time
171
     *
172
     * Normalizes cache TTL handling strings and {@see DateInterval} objects.
173
     * @param int|string|DateInterval|null $ttl raw TTL.
174
     * @return int|null TTL value as UNIX timestamp or null meaning infinity
175
     */
176
    private function normalizeTtl($ttl): ?int
177
    {
178
        if ($ttl instanceof DateInterval) {
179
            return (new DateTime('@0'))->add($ttl)->getTimestamp();
180
        }
181
182
        if (is_string($ttl)) {
183
            return (int)$ttl;
184
        }
185
186
        return $ttl;
187
    }
188
189
    /**
190
     * Converts iterable to array. If provided value is not iterable it throws an InvalidArgumentException
191
     * @param $iterable
192
     * @return array
193
     */
194
    private function iterableToArray($iterable): array
195
    {
196
        if (!is_iterable($iterable)) {
197
            throw new InvalidArgumentException('Iterable is expected, got ' . gettype($iterable));
198
        }
199
200
        return $iterable instanceof \Traversable ? iterator_to_array($iterable) : (array)$iterable;
201
    }
202
203
    /**
204
     * @param array $servers
205
     */
206
    private function initServers(array $servers): void
207
    {
208
        if ($servers === []) {
209
            $servers = [
210
                [self::DEFAULT_SERVER_HOST, self::DEFAULT_SERVER_PORT, self::DEFAULT_SERVER_WEIGHT],
211
            ];
212
        }
213
214
        if ($this->persistentId !== '') {
215
            $servers = $this->getNewServers($servers);
216
        }
217
218
        $success = $this->cache->addServers($servers);
219
220
        if (!$success) {
221
            throw new CacheException('An error occurred while adding servers to the server pool.');
222
        }
223
    }
224
225
    /**
226
     * Returns the list of the servers that are not in the pool.
227
     * @param array $servers
228
     * @return array
229
     */
230
    private function getNewServers(array $servers): array
231
    {
232
        $existingServers = [];
233
        foreach ($this->cache->getServerList() as $existingServer) {
234
            $existingServers[$existingServer['host'] . ':' . $existingServer['port']] = true;
235
        }
236
237
        $newServers = [];
238
        foreach ($servers as $server) {
239
            $serverAddress = $server[0] . ':' . $server[1];
240
            if (!array_key_exists($serverAddress, $existingServers)) {
241
                $newServers[] = $server;
242
            }
243
        }
244
245
        return $newServers;
246
    }
247
248
    /**
249
     * Validates servers format
250
     * @param array $servers
251
     */
252
    private function validateServers(array $servers): void
253
    {
254
        foreach ($servers as $server) {
255
            if (!is_array($server) || !isset($server[0], $server[1])) {
256
                throw new CacheException('Each entry in servers parameter is supposed to be an array containing hostname, port, and, optionally, weight of the server.');
257
            }
258
        }
259
    }
260
261
    /**
262
     * @param $key
263
     */
264
    private function validateKey($key): void
265
    {
266
        if (!\is_string($key)) {
267
            throw new InvalidArgumentException('Invalid key value.');
268
        }
269
    }
270
271
    /**
272
     * @param array $keys
273
     */
274
    private function validateKeys(array $keys): void
275
    {
276
        foreach ($keys as $key) {
277
            $this->validateKey($key);
278
        }
279
    }
280
281
    /**
282
     * @param array $values
283
     */
284
    private function validateKeysOfValues(array $values): void
285
    {
286
        $keys = array_map('strval', array_keys($values));
287
        $this->validateKeys($keys);
288
    }
289
}
290