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