Passed
Push — master ( bf04d1...6e32e3 )
by Sergei
03:14
created

MemorySimpleCache::iterableToArray()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3.1406

Importance

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