Passed
Pull Request — master (#30)
by Evgeniy
02:06
created

Memcached::normalizeServers()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 16
ccs 8
cts 8
cp 1
rs 9.6111
cc 5
nc 3
nop 1
crap 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Cache\Memcached;
6
7
use DateInterval;
8
use DateTime;
9
use Psr\SimpleCache\CacheInterface;
0 ignored issues
show
Bug introduced by
The type Psr\SimpleCache\CacheInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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