Passed
Branch master (0b4094)
by Alexander
01:32
created

Memcached::addServers()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 6.3949

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 12
ccs 7
cts 9
cp 0.7778
rs 9.2222
cc 6
nc 6
nop 2
crap 6.3949

2 Methods

Rating   Name   Duplication   Size   Complexity  
A Memcached::clear() 0 3 1
A Memcached::getMultiple() 0 4 1
1
<?php
2
3
namespace Yiisoft\Cache;
4
5
use DateInterval;
6
use DateTime;
7
use Psr\SimpleCache\CacheInterface;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Yiisoft\Cache\CacheInterface. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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