Passed
Pull Request — master (#682)
by Alexander
02:10
created

SchemaCache::getOrSet()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 8
c 1
b 0
f 0
nc 3
nop 3
dl 0
loc 16
ccs 4
cts 4
cp 1
crap 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Cache;
6
7
use DateInterval;
8
use Psr\SimpleCache\CacheInterface;
9
use Psr\SimpleCache\InvalidArgumentException;
10
use RuntimeException;
11
use Yiisoft\Db\Exception\PsrInvalidArgumentException;
12
13
use function in_array;
14
use function is_int;
15
use function is_string;
16
use function json_encode;
17
use function json_last_error_msg;
18
use function mb_strlen;
19
use function md5;
20
use function strpbrk;
21 3029
22
/**
23 3029
 * Implements a cache for the database schema information.
24 3029
 *
25
 * The {@see \Yiisoft\Db\Schema\AbstractSchema} retrieves information about the database schema from the database server
26
 * and stores it in the cache for faster access. When the {@see \Yiisoft\Db\Schema\AbstractSchema} needs to retrieve
27
 * information about the database schema, it first checks the cache using {@see SchemaCache}. If the information is
28
 * not in the cache, the Schema retrieves it from the database server and stores it in the cache using the
29
 * {@see SchemaCache}.
30
 *
31 1567
 * {@see \Yiisoft\Db\Schema\AbstractSchema} uses this implementation to cache table metadata.
32
 */
33 1567
final class SchemaCache
34 1567
{
35
    private int|null|DateInterval $duration = 3600;
36 1635
    private bool $enabled = true;
37
    private array $exclude = [];
38 1635
39 1635
    /**
40 1635
     * @param CacheInterface $psrCache PSR-16 cache implementation to use.
41
     *
42
     * @link https://www.php-fig.org/psr/psr-16/
43
     */
44
    public function __construct(private CacheInterface $psrCache)
45
    {
46 1567
    }
47
48 1567
    /**
49 1567
     * Remove a value with the specified key from cache.
50 1567
     *
51
     * @param mixed $key A key identifying the value to delete from cache.
52
     *
53
     * @throws InvalidArgumentException
54
     */
55
    public function remove(mixed $key): void
56
    {
57 1622
        $stringKey = $this->normalize($key);
58
        $this->psrCache->delete($stringKey);
59 1622
    }
60
61
    /**
62
     * Retrieve value from cache.
63
     *
64
     * @param mixed $key The key identifying the value to cache.
65
     * @throws InvalidArgumentException
66
     * @return mixed Cache value.
67
     */
68
    public function get(mixed $key): mixed
69 1622
    {
70
        $stringKey = $this->normalize($key);
71 1622
        return $this->psrCache->get($stringKey);
72
    }
73
74
    /**
75
     * Persists data in the cache, uniquely referenced by a key with an optional tag.
76
     *
77
     * @param mixed $key The key of the item to store.
78
     * @param mixed $value The value of the item to store.
79 96
     * @param string|null $tag Cache tag.
80
     *
81 96
     * @throws InvalidArgumentException If the $key string isn't a legal value.
82 96
     * @throws RuntimeException If cache value isn't set.
83
     */
84
    public function set(mixed $key, mixed $value, string $tag = null): void
85
    {
86
        $stringKey = $this->normalize($key);
87
88
        if ($this->psrCache->set($stringKey, $value, $this->duration)) {
89 1640
            $this->addToTag($stringKey, $tag);
90
        }
91 1640
92
        throw new RuntimeException('Cache value not set.');
93
    }
94
95
    /**
96
     * @return DateInterval|int|null The number of seconds that table metadata can remain valid in cache.
97
     */
98
    public function getDuration(): int|null|DateInterval
99
    {
100
        return $this->duration;
101
    }
102
103 39
    /**
104
     * @param string $value The table name.
105 39
     *
106 39
     * @return bool Whether to exclude the table from caching.
107
     */
108
    public function isExcluded(string $value): bool
109
    {
110
        return in_array($value, $this->exclude, true);
111
    }
112
113
    /**
114
     * Invalidates all the cached values associated with any of the specified tags.
115
     *
116
     * @param string $cacheTag The cache tag used to identify the values to invalidate.
117
     *
118
     * @throws InvalidArgumentException
119
     */
120
    public function invalidate(string $cacheTag): void
121
    {
122
        if (empty($cacheTag)) {
123
            return;
124
        }
125
126
        /** @psalm-var string[] $data */
127
        $data = $this->psrCache->get($cacheTag, []);
128
129
        foreach ($data as $key) {
130
            $this->psrCache->delete($key);
131
        }
132
    }
133
134
    /**
135
     * Return true if SchemaCache is active.
136
     */
137
    public function isEnabled(): bool
138
    {
139
        return $this->enabled;
140
    }
141
142
    /**
143
     * Whether to enable schema caching.
144
     *
145
     * @param bool $value Whether to enable schema caching.
146
     *
147
     * @see setDuration()
148
     * @see setExclude()
149
     */
150
    public function setEnable(bool $value): void
151
    {
152
        $this->enabled = $value;
153
    }
154
155
    /**
156
     * Number of seconds that table metadata can remain valid in cache. Use 'null' to indicate that the cached data will
157
     * never expire.
158
     *
159
     * @param DateInterval|int|null $value The number of seconds that table metadata can remain valid in cache.
160
     *
161
     * @see setEnable()
162
     */
163
    public function setDuration(int|null|DateInterval $value): void
164
    {
165
        $this->duration = $value;
166
    }
167
168
    /**
169
     * List of tables not to cache metadata for.
170
     *
171
     * Defaults to an empty array. The table names may contain schema prefix, if any. Don't quote the table names.
172
     *
173
     * @param array $value The table names.
174
     *
175
     * @see setEnable()
176
     */
177
    public function setExclude(array $value): void
178
    {
179
        $this->exclude = $value;
180
    }
181
182
    /**
183
     * Normalizes the cache key from a given key.
184
     *
185
     * If the given key is a string that doesn't contain characters `{}()/\@:` and no more than 64 characters, then the
186
     * key will be returned back as it's, integers will be converted to strings.
187
     *
188
     * Otherwise, a normalized key is generated by encoding the given key into JSON and applying MD5 hashing.
189
     *
190
     * @link https://www.php-fig.org/psr/psr-16/#12-definitions
191
     *
192
     * @param mixed $key A key to normalize.
193
     *
194
     * @throws InvalidArgumentException For invalid key.
195
     *
196
     * @return string The normalized cache key.
197
     */
198
    private function normalize(mixed $key): string
199
    {
200
        if (is_string($key) || is_int($key)) {
201
            $key = (string)$key;
202
            $length = mb_strlen($key, '8bit');
203
            return (strpbrk($key, '{}()/\@:') || $length < 1 || $length > 64) ? md5($key) : $key;
204
        }
205
206
        $key = json_encode($key);
207
208
        if (!$key) {
209
            throw new PsrInvalidArgumentException('Invalid key. ' . json_last_error_msg());
210
        }
211
212
        return md5($key);
213
    }
214
215
    /**
216
     * Add key to tag. If tag is empty, do nothing.
217
     *
218
     * @throws InvalidArgumentException
219
     */
220
    private function addToTag(string $key, string $cacheTag = null): void
221
    {
222
        if (empty($cacheTag)) {
223
            return;
224
        }
225
226
        /** @psalm-var string[] $data */
227
        $data = $this->psrCache->get($cacheTag, []);
228
        $data[] = $key;
229
        $this->psrCache->set($cacheTag, $data);
230
    }
231
}
232