Test Failed
Pull Request — master (#16)
by Alexander
11:49 queued 09:08
created

MemorySimpleCache::ttlToExpiration()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

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
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
    public function __construct(array $cacheData = [])
26
    {
27
        $this->setMultiple($cacheData);
28
    }
29
30
    /**
31
     * @return mixed
32
     */
33
    public function get($key, $default = null)
34
    {
35
        $this->validateKey($key);
36
        /** @psalm-var string $key */
37
        if (array_key_exists($key, $this->cache) && !$this->isExpired($key)) {
38
            /** @psalm-var mixed $value */
39
            $value = $this->cache[$key][0];
40
            if (is_object($value)) {
41
                $value = clone $value;
42
            }
43
44
            return $value;
45
        }
46
47
        return $default;
48
    }
49
50
    public function set($key, $value, $ttl = null): bool
51
    {
52
        $this->validateKey($key);
53
        /** @psalm-var string $key */
54
        $expiration = $this->ttlToExpiration($ttl);
55
        if ($expiration < 0) {
56
            return $this->delete($key);
57
        }
58
        if (is_object($value)) {
59
            $value = clone $value;
60
        }
61
        $this->cache[$key] = [$value, $expiration];
62
        return true;
63
    }
64
65
    public function delete($key): bool
66
    {
67
        $this->validateKey($key);
68
        /** @psalm-var string $key */
69
        unset($this->cache[$key]);
70
        return $this->returnOnDelete;
71
    }
72
73
    public function clear(): bool
74
    {
75
        $this->cache = [];
76
        return true;
77
    }
78
79
    /**
80
     * @return mixed[]
81
     */
82
    public function getMultiple($keys, $default = null): iterable
83
    {
84
        $keys = $this->iterableToArray($keys);
85
        $this->validateKeys($keys);
86
        /** @psalm-var string[] $keys */
87
        $result = [];
88
        foreach ($keys as $key) {
89
            /** @psalm-var mixed */
90
            $result[$key] = $this->get($key, $default);
91
        }
92
        return $result;
93
    }
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 true;
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) || 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