Passed
Pull Request — master (#16)
by Aleksei
02:13
created

MemorySimpleCache::ttlToExpiration()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 8
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 13
ccs 8
cts 8
cp 1
crap 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Test\Support\SimpleCache;
6
7
use DateInterval;
8
use DateTime;
9
use Psr\SimpleCache\CacheInterface;
10
use Traversable;
11
use Yiisoft\Test\Support\SimpleCache\Exception\InvalidArgumentException;
12
use function array_key_exists;
13
use function is_object;
14
use function is_string;
15
16
final class MemorySimpleCache implements CacheInterface
17
{
18
    protected const EXPIRATION_INFINITY = 0;
19
    protected const EXPIRATION_EXPIRED = -1;
20
21
    /** @var array<string, array<int, mixed>> */
22
    protected array $cache = [];
23
    public bool $returnOnDelete = true;
24
25 285
    public function __construct(array $cacheData = [])
26
    {
27 285
        $this->setMultiple($cacheData);
28 285
    }
29
30
    /**
31
     * @return mixed
32
     */
33 159
    public function get($key, $default = null)
34
    {
35 159
        $this->validateKey($key);
36
        /** @psalm-var string $key */
37 143
        if (array_key_exists($key, $this->cache) && !$this->isExpired($key)) {
38
            /** @psalm-var mixed $value */
39 117
            $value = $this->cache[$key][0];
40 117
            if (is_object($value)) {
41 16
                $value = clone $value;
42
            }
43
44 117
            return $value;
45
        }
46
47 53
        return $default;
48
    }
49
50 200
    public function set($key, $value, $ttl = null): bool
51
    {
52 200
        $this->validateKey($key);
53
        /** @psalm-var string $key */
54 184
        $expiration = $this->ttlToExpiration($ttl);
55 184
        if ($expiration < 0) {
56 2
            return $this->delete($key);
57
        }
58 184
        if (is_object($value)) {
59 42
            $value = clone $value;
60
        }
61 184
        $this->cache[$key] = [$value, $expiration];
62 184
        return true;
63
    }
64
65 46
    public function delete($key): bool
66
    {
67 46
        $this->validateKey($key);
68
        /** @psalm-var string $key */
69 30
        unset($this->cache[$key]);
70 30
        return $this->returnOnDelete;
71
    }
72
73 167
    public function clear(): bool
74
    {
75 167
        $this->cache = [];
76 167
        return true;
77
    }
78
79
    /**
80
     * @return mixed[]
81
     */
82 30
    public function getMultiple($keys, $default = null): iterable
83
    {
84 30
        $keys = $this->iterableToArray($keys);
85 29
        $this->validateKeys($keys);
86
        /** @psalm-var string[] $keys */
87 15
        $result = [];
88 15
        foreach ($keys as $key) {
89
            /** @psalm-var mixed */
90 15
            $result[$key] = $this->get($key, $default);
91
        }
92 15
        return $result;
93
    }
94
95 285
    public function setMultiple($values, $ttl = null): bool
96
    {
97 285
        $values = $this->iterableToArray($values);
98 285
        $this->validateKeysOfValues($values);
99
        /**
100
         * @psalm-var mixed $value
101
         */
102 285
        foreach ($values as $key => $value) {
103 26
            $this->set((string) $key, $value, $ttl);
104
        }
105 285
        return true;
106
    }
107
108 18
    public function deleteMultiple($keys): bool
109
    {
110 18
        $keys = $this->iterableToArray($keys);
111 17
        $this->validateKeys($keys);
112
        /** @var string[] $keys */
113 3
        foreach ($keys as $key) {
114 3
            $this->delete($key);
115
        }
116 3
        return $this->returnOnDelete;
117
    }
118
119 70
    public function has($key): bool
120
    {
121 70
        $this->validateKey($key);
122
        /** @psalm-var string $key */
123 54
        return isset($this->cache[$key]) && !$this->isExpired($key);
124
    }
125
126
    /**
127
     * Get stored data
128
     *
129
     * @return array<array-key, mixed>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key, mixed> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key, mixed>.
Loading history...
130
     */
131 1
    public function getValues(): array
132
    {
133 1
        $result = [];
134 1
        foreach ($this->cache as $key => $value) {
135
            /** @psalm-var mixed */
136 1
            $result[$key] = $value[0];
137
        }
138 1
        return $result;
139
    }
140
141
    /**
142
     * Checks whether item is expired or not
143
     */
144 133
    private function isExpired(string $key): bool
145
    {
146 133
        return $this->cache[$key][1] !== 0 && $this->cache[$key][1] <= time();
147
    }
148
149
    /**
150
     * Converts TTL to expiration
151
     *
152
     * @param DateInterval|int|null $ttl
153
     *
154
     * @return int
155
     */
156 184
    private function ttlToExpiration($ttl): int
157
    {
158 184
        $ttl = $this->normalizeTtl($ttl);
159
160 184
        if ($ttl === null) {
161 182
            $expiration = self::EXPIRATION_INFINITY;
162 6
        } elseif ($ttl <= 0) {
163 2
            $expiration = self::EXPIRATION_EXPIRED;
164
        } else {
165 4
            $expiration = $ttl + time();
166
        }
167
168 184
        return $expiration;
169
    }
170
171
    /**
172
     * Normalizes cache TTL handling strings and {@see DateInterval} objects.
173
     *
174
     * @param DateInterval|int|string|null $ttl raw TTL.
175
     *
176
     * @return int|null TTL value as UNIX timestamp or null meaning infinity
177
     */
178 184
    private function normalizeTtl($ttl): ?int
179
    {
180 184
        if ($ttl instanceof DateInterval) {
181 2
            return (new DateTime('@0'))->add($ttl)->getTimestamp();
182
        }
183
184 184
        if (is_string($ttl)) {
185
            return (int)$ttl;
186
        }
187
188 184
        return $ttl;
189
    }
190
191
    /**
192
     * @param mixed $iterable
193
     *
194
     * Converts iterable to array. If provided value is not iterable it throws an InvalidArgumentException
195
     */
196 285
    private function iterableToArray($iterable): array
197
    {
198 285
        if (!is_iterable($iterable)) {
199 3
            throw new InvalidArgumentException(sprintf('Iterable is expected, got %s.', gettype($iterable)));
200
        }
201 285
        return $iterable instanceof Traversable ? iterator_to_array($iterable) : $iterable;
202
    }
203
204
    /**
205
     * @param mixed $key
206
     */
207 278
    private function validateKey($key): void
208
    {
209 278
        if (!is_string($key) || $key === '' || strpbrk($key, '{}()/\@:')) {
210 106
            throw new InvalidArgumentException('Invalid key value.');
211
        }
212 200
    }
213
214
    /**
215
     * @param mixed[] $keys
216
     */
217 285
    private function validateKeys(array $keys): void
218
    {
219
        /** @psalm-var mixed $key */
220 285
        foreach ($keys as $key) {
221 68
            $this->validateKey($key);
222
        }
223 285
    }
224
225 285
    private function validateKeysOfValues(array $values): void
226
    {
227 285
        $keys = array_map('strval', array_keys($values));
228 285
        $this->validateKeys($keys);
229 285
    }
230
}
231