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

MemorySimpleCache::validateKeysOfValues()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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