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

Memcached::has()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
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
128 2
        if ($this->cache->getResultCode() === \Memcached::RES_NOTFOUND) {
129 1
            return false;
130
        }
131
132 1
        return true;
133
    }
134
135 21
    public function has($key): bool
136
    {
137 21
        $this->validateKey($key);
138 13
        $this->cache->get($key);
139 13
        return $this->cache->getResultCode() === \Memcached::RES_SUCCESS;
140
    }
141
142
    /**
143
     * Normalizes cache TTL handling `null` value, strings and {@see DateInterval} objects.
144
     *
145
     * @param DateInterval|int|string|null $ttl The raw TTL.
146
     *
147
     * @return int TTL value as UNIX timestamp.
148
     *
149
     * @see https://secure.php.net/manual/en/memcached.expiration.php
150
     */
151 92
    private function normalizeTtl($ttl): int
152
    {
153 92
        if ($ttl === null) {
154 82
            return self::TTL_INFINITY;
155
        }
156
157 12
        if ($ttl instanceof DateInterval) {
158 3
            $ttl = (new DateTime('@0'))->add($ttl)->getTimestamp();
159
        }
160
161 12
        $ttl = (int) $ttl;
162
163 12
        if ($ttl > 2592000) {
164 1
            return $ttl + time();
165
        }
166
167 11
        return $ttl > 0 ? $ttl : self::TTL_EXPIRED;
168
    }
169
170
    /**
171
     * Converts iterable to array. If provided value is not iterable it throws an InvalidArgumentException.
172
     *
173
     * @param mixed $iterable
174
     *
175
     * @return array
176
     */
177 51
    private function iterableToArray($iterable): array
178
    {
179 51
        if (!is_iterable($iterable)) {
180 24
            throw new InvalidArgumentException('Iterable is expected, got ' . gettype($iterable));
181
        }
182
183
        /** @psalm-suppress RedundantCast */
184 27
        return $iterable instanceof Traversable ? iterator_to_array($iterable) : (array) $iterable;
185
    }
186
187
    /**
188
     * @param array $servers
189
     * @param string $persistentId
190
     *
191
     * @throws CacheException If an error occurred when adding servers to the server pool.
192
     * @throws InvalidArgumentException If the servers format is incorrect.
193
     */
194 177
    private function initServers(array $servers, string $persistentId): void
195
    {
196 177
        $servers = $this->normalizeServers($servers);
197
198 171
        if ($persistentId !== '') {
199 1
            $servers = $this->getNewServers($servers);
200
        }
201
202 171
        if (!$this->cache->addServers($servers)) {
203 1
            throw new CacheException('An error occurred while adding servers to the server pool.');
204
        }
205 171
    }
206
207
    /**
208
     * Returns the list of the servers that are not in the pool.
209
     *
210
     * @param array $servers
211
     *
212
     * @return array
213
     */
214 2
    private function getNewServers(array $servers): array
215
    {
216 2
        $existingServers = [];
217 2
        $newServers = [];
218
219 2
        foreach ($this->cache->getServerList() as $existingServer) {
220 1
            $existingServers["{$existingServer['host']}:{$existingServer['port']}"] = true;
221
        }
222
223 2
        foreach ($servers as $server) {
224 2
            if (!array_key_exists("{$server[0]}:{$server[1]}", $existingServers)) {
225 2
                $newServers[] = $server;
226
            }
227
        }
228
229 2
        return $newServers;
230
    }
231
232
    /**
233
     * Validates and normalizes the format of the servers.
234
     *
235
     * @param array $servers The raw servers.
236
     *
237
     * @throws InvalidArgumentException If the servers format is incorrect.
238
     *
239
     * @return array The normalized servers.
240
     */
241 177
    private function normalizeServers(array $servers): array
242
    {
243 177
        $normalized = [];
244
245 177
        foreach ($servers as $server) {
246 176
            if (!is_array($server) || !isset($server['host'], $server['port'])) {
247 6
                throw new InvalidArgumentException(
248
                    'Each entry in servers parameter is supposed to be an array'
249 6
                    . ' containing hostname, port, and, optionally, weight of the server.',
250
                );
251
            }
252
253 170
            $normalized[] = [$server['host'], $server['port'], $server['weight'] ?? self::DEFAULT_SERVER_WEIGHT];
254
        }
255
256 171
        return $normalized ?: [[self::DEFAULT_SERVER_HOST, self::DEFAULT_SERVER_PORT, self::DEFAULT_SERVER_WEIGHT]];
257
    }
258
259
    /**
260
     * @param mixed $key
261
     */
262 133
    private function validateKey($key): void
263
    {
264 133
        if (!is_string($key) || $key === '' || strpbrk($key, '{}()/\@:')) {
265 48
            throw new InvalidArgumentException('Invalid key value.');
266
        }
267 85
    }
268
269
    /**
270
     * @param array $keys
271
     */
272 27
    private function validateKeys(array $keys): void
273
    {
274 27
        foreach ($keys as $key) {
275 27
            $this->validateKey($key);
276
        }
277 11
    }
278
279
    /**
280
     * @param array $values
281
     */
282 10
    private function validateKeysOfValues(array $values): void
283
    {
284 10
        $keys = array_map('strval', array_keys($values));
285 10
        $this->validateKeys($keys);
286 10
    }
287
}
288