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