Passed
Pull Request — master (#600)
by Aleksei
06:54 queued 01:04
created

ArrayCache::getValues()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 8
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Tests\Cache;
6
7
use DateInterval;
8
use DateTime;
9
use Psr\SimpleCache\CacheInterface;
10
use Traversable;
11
12
final class ArrayCache implements CacheInterface
13
{
14
    protected const EXPIRATION_INFINITY = 0;
15
    protected const EXPIRATION_EXPIRED = -1;
16
17
    public bool $returnOnSet = true;
18
    public bool $returnOnDelete = true;
19
    public bool $returnOnClear = true;
20
21
    /** @var array<string, array<int, mixed>> */
22
    protected array $cache = [];
23
24
    public function __construct(array $cacheData = [])
25
    {
26
        $this->setMultiple($cacheData);
27
    }
28
29
    public function get($key, $default = null)
30
    {
31
        $this->validateKey($key);
32
        if (\array_key_exists($key, $this->cache) && !$this->isExpired($key)) {
33
            /** @psalm-var mixed $value */
34
            $value = $this->cache[$key][0];
35
            if (\is_object($value)) {
36
                $value = clone $value;
37
            }
38
39
            return $value;
40
        }
41
42
        return $default;
43
    }
44
45
    public function set($key, $value, $ttl = null): bool
46
    {
47
        $this->validateKey($key);
48
        $expiration = $this->ttlToExpiration($ttl);
49
        if ($expiration < 0) {
50
            return $this->delete($key);
51
        }
52
        if (\is_object($value)) {
53
            $value = clone $value;
54
        }
55
        $this->cache[$key] = [$value, $expiration];
56
        return $this->returnOnSet;
57
    }
58
59
    public function delete($key): bool
60
    {
61
        $this->validateKey($key);
62
        unset($this->cache[$key]);
63
        return $this->returnOnDelete;
64
    }
65
66
    public function clear(): bool
67
    {
68
        $this->cache = [];
69
        return $this->returnOnClear;
70
    }
71
72
    /**
73
     * @param iterable $keys
74
     * @param mixed $default
75
     *
76
     * @return mixed[]
77
     */
78
    public function getMultiple($keys, $default = null): iterable
79
    {
80
        $keys = $this->iterableToArray($keys);
81
        $this->validateKeys($keys);
82
        /** @psalm-var string[] $keys */
83
        $result = [];
84
        foreach ($keys as $key) {
85
            /** @psalm-var mixed */
86
            $result[$key] = $this->get($key, $default);
87
        }
88
        return $result;
89
    }
90
91
    /**
92
     * @param iterable $values
93
     * @param DateInterval|int|null $ttl
94
     */
95
    public function setMultiple($values, $ttl = null): bool
96
    {
97
        $values = $this->iterableToArray($values);
98
        $this->validateKeysOfValues($values);
99
        /**
100
         * @psalm-var mixed $value
101
         */
102
        foreach ($values as $key => $value) {
103
            $this->set((string)$key, $value, $ttl);
104
        }
105
        return $this->returnOnSet;
106
    }
107
108
    public function deleteMultiple($keys): bool
109
    {
110
        $keys = $this->iterableToArray($keys);
111
        $this->validateKeys($keys);
112
        /** @var string[] $keys */
113
        foreach ($keys as $key) {
114
            $this->delete($key);
115
        }
116
        return $this->returnOnDelete;
117
    }
118
119
    public function has($key): bool
120
    {
121
        $this->validateKey($key);
122
        /** @psalm-var string $key */
123
        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
    public function getValues(): array
132
    {
133
        $result = [];
134
        foreach ($this->cache as $key => $value) {
135
            /** @psalm-var mixed */
136
            $result[$key] = $value[0];
137
        }
138
        return $result;
139
    }
140
141
    /**
142
     * Checks whether item is expired or not
143
     */
144
    private function isExpired(string $key): bool
145
    {
146
        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
    private function ttlToExpiration($ttl): int
157
    {
158
        $ttl = $this->normalizeTtl($ttl);
159
160
        if ($ttl === null) {
161
            $expiration = self::EXPIRATION_INFINITY;
162
        } elseif ($ttl <= 0) {
163
            $expiration = self::EXPIRATION_EXPIRED;
164
        } else {
165
            $expiration = $ttl + time();
166
        }
167
168
        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
    private function normalizeTtl($ttl): ?int
179
    {
180
        if ($ttl instanceof DateInterval) {
181
            return (new DateTime('@0'))->add($ttl)->getTimestamp();
182
        }
183
184
        if (\is_string($ttl)) {
185
            return (int)$ttl;
186
        }
187
188
        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
    private function iterableToArray($iterable): array
197
    {
198
        if (!is_iterable($iterable)) {
199
            throw new \InvalidArgumentException(\sprintf('Iterable is expected, got %s.', \gettype($iterable)));
200
        }
201
        return $iterable instanceof Traversable ? \iterator_to_array($iterable) : $iterable;
202
    }
203
204
    /**
205
     * @param mixed $key
206
     */
207
    private function validateKey($key): void
208
    {
209
        if (!\is_string($key) || $key === '' || \strpbrk($key, '{}()/\@:')) {
210
            throw new \InvalidArgumentException('Invalid key value.');
211
        }
212
    }
213
214
    /**
215
     * @param mixed[] $keys
216
     */
217
    private function validateKeys(array $keys): void
218
    {
219
        /** @psalm-var mixed $key */
220
        foreach ($keys as $key) {
221
            $this->validateKey($key);
222
        }
223
    }
224
225
    private function validateKeysOfValues(array $values): void
226
    {
227
        $keys = \array_map('strval', \array_keys($values));
228
        $this->validateKeys($keys);
229
    }
230
}
231