Completed
Pull Request — master (#30)
by Alexander
09:17 queued 07:44
created

Memcached   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 216
Duplicated Lines 0 %

Test Coverage

Coverage 96%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 70
c 1
b 0
f 0
dl 0
loc 216
ccs 72
cts 75
cp 0.96
rs 9.6
wmc 35

17 Methods

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