Completed
Pull Request — master (#30)
by Alexander
04:25
created

Memcached::getNewServers()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.0312

Importance

Changes 0
Metric Value
cc 4
eloc 9
c 0
b 0
f 0
nc 6
nop 1
dl 0
loc 16
rs 9.9666
ccs 7
cts 8
cp 0.875
crap 4.0312
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
    public const EXPIRATION_INFINITY = 0;
26
    public const DEFAULT_SERVER_HOST = '127.0.0.1';
27
    public const DEFAULT_SERVER_PORT = 11211;
28
    public const DEFAULT_SERVER_WEIGHT = 1;
29
30
    /**
31
     * @var \Memcached the Memcached instance
32
     */
33
    private $cache;
34
35
    /**
36
     * @var string an ID that identifies a Memcached instance.
37
     * By default the Memcached instances are destroyed at the end of the request. To create an instance that
38
     * persists between requests, you may specify a unique ID for the instance. All instances created with the
39
     * same ID will share the same connection.
40
     * @see https://www.php.net/manual/en/memcached.construct.php
41
     */
42
    private $persistentId;
43
44
    /**
45
     * @param string $persistentId By default the Memcached instances are destroyed at the end of the request. To create an
46
     * instance that persists between requests, use persistent_id to specify a unique ID for the instance. All instances
47
     * created with the same persistent_id will share the same connection.
48
     * @param array $servers list of memcached servers that will be added to the server pool
49
     * @see https://www.php.net/manual/en/memcached.construct.php
50
     * @see https://www.php.net/manual/en/memcached.addservers.php
51
     */
52
    public function __construct($persistentId = '', array $servers = [])
53
    {
54
        $this->persistentId = $persistentId;
55
        $this->initCache();
56
        $this->initServers($servers);
57
    }
58
59
    public function get($key, $default = null)
60
    {
61
        $value = $this->cache->get($key);
62
63
        if ($this->cache->getResultCode() === \Memcached::RES_SUCCESS) {
64
            return $value;
65
        }
66 17
67
        return $default;
68 17
    }
69
70 17
    public function set($key, $value, $ttl = null): bool
71 17
    {
72
        $expiration = $this->ttlToExpiration($ttl);
73
        if ($expiration < 0) {
74 17
            return $this->delete($key);
75
        }
76 17
        return $this->cache->set($key, $value, $expiration);
77
    }
78
79
    public function delete($key): bool
80
    {
81
        return $this->cache->delete($key);
82
    }
83
84
    public function clear(): bool
85 17
    {
86
        return $this->cache->flush();
87 17
    }
88 17
89
    public function getMultiple($keys, $default = null)
90
    {
91
        $values = $this->cache->getMulti($this->iterableToArray($keys));
92
        return array_merge(array_fill_keys($this->iterableToArray($keys), $default), $values);
93 17
    }
94 17
95 17
    public function setMultiple($values, $ttl = null): bool
96 17
    {
97
        $expiration = $this->ttlToExpiration($ttl);
98
        return $this->cache->setMulti($this->iterableToArray($values), $expiration);
99
    }
100
101
    public function deleteMultiple($keys): bool
102
    {
103
        foreach ($this->cache->deleteMulti($this->iterableToArray($keys)) as $result) {
104
            if ($result === false) {
105
                return false;
106 17
            }
107
        }
108 17
        return true;
109 17
    }
110
111
    public function has($key): bool
112
    {
113 17
        $this->cache->get($key);
114 17
        return $this->cache->getResultCode() === \Memcached::RES_SUCCESS;
115
    }
116
117
    /**
118 17
     * Returns underlying \Memcached instance
119
     * @return \Memcached
120
     */
121
    public function getCache(): \Memcached
122
    {
123 17
        return $this->cache;
124
    }
125
126
    /**
127
     * Inits Memcached instance
128
     */
129
    private function initCache(): void
130
    {
131
        $this->cache = new \Memcached($this->persistentId);
132
    }
133
134
    /**
135
     * Converts TTL to expiration
136
     * @param $ttl
137
     * @return int
138
     */
139
    private function ttlToExpiration($ttl): int
140
    {
141
        $ttl = $this->normalizeTtl($ttl);
142
143
        if ($ttl === null) {
144
            $expiration = static::EXPIRATION_INFINITY;
145
        } elseif ($ttl <= 0) {
146
            $expiration = -1;
147
        } else {
148
            $expiration = $ttl + time();
149
        }
150
151
        return $expiration;
152
    }
153
154
    /**
155
     * Normalizes cache TTL handling `null` value and {@see DateInterval} objects.
156
     * @param int|DateInterval|null $ttl raw TTL.
157
     * @return int|null TTL value as UNIX timestamp or null meaning infinity
158
     */
159
    private function normalizeTtl($ttl): ?int
160
    {
161
        if ($ttl instanceof DateInterval) {
162
            try {
163
                return (new DateTime('@0'))->add($ttl)->getTimestamp();
164
            } catch (Exception $e) {
165
                return null;
166
            }
167
        }
168
169
        return $ttl;
170
    }
171
172
    /**
173
     * Converts iterable to array
174
     * @param iterable $iterable
175
     * @return array
176
     */
177
    private function iterableToArray(iterable $iterable): array
178
    {
179
        return $iterable instanceof \Traversable ? iterator_to_array($iterable) : (array)$iterable;
180
    }
181
182
    /**
183
     * @param array $servers
184
     */
185
    private function initServers(array $servers): void
186 15
    {
187
        if (empty($servers)) {
188 15
            $servers = [
189
                [self::DEFAULT_SERVER_HOST, self::DEFAULT_SERVER_PORT, self::DEFAULT_SERVER_WEIGHT],
190 15
            ];
191 13
        }
192
193
        if ($this->persistentId !== '') {
194 9
            $servers = $this->getNewServers($servers);
195
        }
196
197 3
        $success = $this->cache->addServers($servers);
198
199 3
        if (!$success) {
200
            throw new InvalidConfigException('An error occurred while adding servers to the server pool.');
201 3
        }
202 3
    }
203
204
    /**
205 1
     * Returns the list of the servers that are not in the pool.
206
     * @param array $servers
207
     * @return array
208 13
     */
209
    private function getNewServers(array $servers): array
210 13
    {
211 13
        $existingServers = [];
212
        foreach ($this->cache->getServerList() as $existingServer) {
213
            $existingServers[$existingServer['host'] . ':' . $existingServer['port']] = true;
214
        }
215 13
216
        $newServers = [];
217
        foreach ($servers as $server) {
218 5
            $serverAddress = $server[0] . ':' . $server[1];
219
            if (!array_key_exists($serverAddress, $existingServers)) {
220 5
                $newServers[] = $server;
221 4
            }
222
        }
223 1
224
        return $newServers;
225 5
    }
226
}
227