ArrayCache   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 180
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 33
eloc 62
c 0
b 0
f 0
dl 0
loc 180
ccs 69
cts 69
cp 1
rs 9.76

15 Methods

Rating   Name   Duplication   Size   Complexity  
A iterableToArray() 0 3 2
A getMultiple() 0 13 2
A get() 0 13 3
A clear() 0 4 1
A deleteMultiple() 0 11 2
A delete() 0 5 1
A set() 0 15 3
A ttlToExpiration() 0 13 3
A normalizeTtl() 0 13 3
A validateKeysOfValues() 0 4 1
A isExpired() 0 3 3
A validateKey() 0 4 4
A has() 0 4 1
A setMultiple() 0 10 2
A validateKeys() 0 4 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Cache;
6
7
use DateInterval;
8
use DateTime;
9
use Traversable;
10
use Yiisoft\Cache\Exception\InvalidArgumentException;
11
12
use function array_keys;
13
use function array_map;
14
use function is_object;
15
use function is_string;
16
use function iterator_to_array;
17
use function strpbrk;
18
use function time;
19
20
/**
21
 * ArrayCache provides caching for the current request only by storing the values in an array.
22
 *
23
 * See {@see \Psr\SimpleCache\CacheInterface} for common cache operations that ArrayCache supports.
24
 */
25
final class ArrayCache implements \Psr\SimpleCache\CacheInterface
26
{
27
    private const EXPIRATION_INFINITY = 0;
28
    private const EXPIRATION_EXPIRED = -1;
29
30
    /** @psalm-var array<string, array{0: mixed, 1: int}> */
31
    private array $cache = [];
32
33 132
    public function get(string $key, mixed $default = null): mixed
34
    {
35 132
        if ($this->has($key)) {
36
            $value = $this->cache[$key][0];
37 88
38
            if (is_object($value)) {
39 88
                return clone $value;
40 8
            }
41
42
            return $value;
43 84
        }
44
45
        return $default;
46 85
    }
47
48
    public function set(string $key, mixed $value, null|int|DateInterval $ttl = null): bool
49 145
    {
50
        $this->validateKey($key);
51 145
        $expiration = $this->ttlToExpiration($ttl);
52 143
53
        if ($expiration < 0) {
54 143
            return $this->delete($key);
55 4
        }
56
57
        if (is_object($value)) {
58 142
            $value = clone $value;
59 21
        }
60
61
        $this->cache[$key] = [$value, $expiration];
62 142
        return true;
63 142
    }
64
65
    public function delete(string $key): bool
66 42
    {
67
        $this->validateKey($key);
68 42
        unset($this->cache[$key]);
69 40
        return true;
70 40
    }
71
72
    public function clear(): bool
73 86
    {
74
        $this->cache = [];
75 86
        return true;
76 86
    }
77
78
    public function getMultiple(iterable $keys, mixed $default = null): iterable
79 33
    {
80
        $keys = $this->iterableToArray($keys);
81 33
        /** @psalm-suppress RedundantCondition */
82
        $this->validateKeys($keys);
83 33
        $results = [];
84 31
85
        foreach ($keys as $key) {
86 31
            $value = $this->get($key, $default);
87
            $results[$key] = $value;
88 31
        }
89
90 31
        return $results;
91
    }
92
93 31
    public function setMultiple(iterable $values, null|int|DateInterval $ttl = null): bool
94
    {
95
        $values = $this->iterableToArray($values);
96 34
        $this->validateKeysOfValues($values);
97
98 34
        foreach ($values as $key => $value) {
99 34
            $this->set((string) $key, $value, $ttl);
100
        }
101
102 34
        return true;
103 34
    }
104
105
    public function deleteMultiple(iterable $keys): bool
106 34
    {
107
        $keys = $this->iterableToArray($keys);
108
        /** @psalm-suppress RedundantCondition */
109 13
        $this->validateKeys($keys);
110
111 13
        foreach ($keys as $key) {
112
            $this->delete($key);
113 13
        }
114
115 11
        return true;
116 11
    }
117
118
    public function has(string $key): bool
119 11
    {
120
        $this->validateKey($key);
121
        return !$this->isExpired($key);
122 136
    }
123
124 136
    /**
125 132
     * Checks whether item is expired or not
126
     */
127
    private function isExpired(string $key): bool
128
    {
129
        return !isset($this->cache[$key]) || ($this->cache[$key][1] !== 0 && $this->cache[$key][1] <= time());
130
    }
131
132
    /**
133
     * Converts TTL to expiration.
134
     */
135 132
    private function ttlToExpiration(DateInterval|int|null $ttl): int
136
    {
137 132
        $ttl = $this->normalizeTtl($ttl);
138
139
        if ($ttl === null) {
140
            return self::EXPIRATION_INFINITY;
141
        }
142
143 146
        if ($ttl <= 0) {
144
            return self::EXPIRATION_EXPIRED;
145 146
        }
146
147 146
        return $ttl + time();
148 130
    }
149
150
    /**
151 21
     * Normalizes cache TTL handling strings and {@see DateInterval} objects.
152 5
     *
153
     * @param DateInterval|int|string|null $ttl Raw TTL.
154
     *
155 17
     * @return int|null TTL value as UNIX timestamp or null meaning infinity
156
     */
157
    private function normalizeTtl(DateInterval|int|string|null $ttl): ?int
158
    {
159
        if ($ttl instanceof DateInterval) {
160
            return (new DateTime('@0'))
161
                ->add($ttl)
162
                ->getTimestamp();
163
        }
164
165 152
        if ($ttl === null) {
166
            return null;
167 152
        }
168 3
169 3
        return (int) $ttl;
170 3
    }
171
172
    /**
173 150
     * Converts iterable to array.
174 131
     *
175
     * @psalm-template T
176
     * @psalm-param iterable<T> $iterable
177 23
     * @psalm-return array<array-key,T>
178
     */
179
    private function iterableToArray(iterable $iterable): array
180
    {
181
        return $iterable instanceof Traversable ? iterator_to_array($iterable) : $iterable;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $iterable instanc...($iterable) : $iterable could return the type iterable which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
182
    }
183
184
    private function validateKey(mixed $key): void
185
    {
186
        if (!is_string($key) || $key === '' || strpbrk($key, '{}()/\@:')) {
187 38
            throw new InvalidArgumentException('Invalid key value.');
188
        }
189 38
    }
190
191
    /**
192 156
     * @psalm-assert string[] $keys
193
     */
194 156
    private function validateKeys(array $keys): void
195 14
    {
196
        foreach ($keys as $key) {
197
            $this->validateKey($key);
198
        }
199
    }
200
201
    private function validateKeysOfValues(array $values): void
202 38
    {
203
        $keys = array_map('\strval', array_keys($values));
204
        $this->validateKeys($keys);
205 38
    }
206
}
207