• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 25908013250

15 May 2026 08:24AM UTC coverage: 88.459% (+0.2%) from 88.299%
25908013250

Pull #10159

github

web-flow
Merge 5bd38b5ce into 170b89a6e
Pull Request #10159: feat: Add support for callable TTLs in cache handlers

6 of 10 new or added lines in 3 files covered. (60.0%)

446 existing lines in 24 files now uncovered.

24114 of 27260 relevant lines covered (88.46%)

219.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

41.27
/system/Test/Mock/MockCache.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Test\Mock;
15

16
use Closure;
17
use CodeIgniter\Cache\CacheInterface;
18
use CodeIgniter\Cache\Handlers\BaseHandler;
19
use CodeIgniter\Cache\LockStoreInterface;
20
use CodeIgniter\Cache\LockStoreProviderInterface;
21
use CodeIgniter\I18n\Time;
22
use PHPUnit\Framework\Assert;
23

24
class MockCache extends BaseHandler implements CacheInterface, LockStoreProviderInterface
25
{
26
    /**
27
     * Mock cache storage.
28
     *
29
     * @var array<string, mixed>
30
     */
31
    protected $cache = [];
32

33
    /**
34
     * Expiration times.
35
     *
36
     * @var array<string, int|null>
37
     */
38
    protected $expirations = [];
39

40
    /**
41
     * If true, will not cache any data.
42
     *
43
     * @var bool
44
     */
45
    protected $bypass = false;
46

47
    private ?MockLockStore $lockStore = null;
48

49
    /**
50
     * Takes care of any handler-specific setup that must be done.
51
     */
52
    public function initialize(): void
53
    {
54
    }
×
55

56
    /**
57
     * Attempts to fetch an item from the cache store.
58
     *
59
     * @param string $key Cache item name
60
     *
61
     * @return bool|null
62
     */
63
    public function get(string $key): mixed
64
    {
65
        $key = static::validateKey($key, $this->prefix);
63✔
66

67
        return $this->cache[$key] ?? null;
63✔
68
    }
69

70
    /**
71
     * Get an item from the cache, or execute the given Closure and store the result.
72
     *
73
     * @return bool|null
74
     */
75
    public function remember(string $key, callable|int $ttl, Closure $callback): mixed
76
    {
77
        $value = $this->get($key);
×
78

79
        if ($value !== null) {
×
80
            return $value;
×
81
        }
82

NEW
83
        $value = $callback();
×
84

NEW
85
        if (is_callable($ttl)) {
×
NEW
86
            $ttl = $ttl($value);
×
87
        }
88

NEW
89
        $this->save($key, $value, $ttl);
×
90

91
        return $value;
×
92
    }
93

94
    /**
95
     * Saves an item to the cache store.
96
     *
97
     * The $raw parameter is only utilized by Mamcache in order to
98
     * allow usage of increment() and decrement().
99
     *
100
     * @param string $key   Cache item name
101
     * @param mixed  $value the data to save
102
     * @param int    $ttl   Time To Live, in seconds (default 60)
103
     */
104
    public function save(string $key, $value, int $ttl = 60): bool
105
    {
106
        if ($this->bypass) {
32✔
107
            return false;
1✔
108
        }
109

110
        $key = static::validateKey($key, $this->prefix);
32✔
111

112
        $this->cache[$key]       = $value;
32✔
113
        $this->expirations[$key] = $ttl > 0 ? Time::now()->getTimestamp() + $ttl : null;
32✔
114

115
        return true;
32✔
116
    }
117

118
    /**
119
     * Deletes a specific item from the cache store.
120
     */
121
    public function delete(string $key): bool
122
    {
123
        $key = static::validateKey($key, $this->prefix);
4✔
124

125
        if (! isset($this->cache[$key])) {
4✔
126
            return false;
3✔
127
        }
128

129
        unset($this->cache[$key], $this->expirations[$key]);
1✔
130

131
        return true;
1✔
132
    }
133

134
    /**
135
     * Deletes items from the cache store matching a given pattern.
136
     */
137
    public function deleteMatching(string $pattern): int
138
    {
139
        $count = 0;
×
140

141
        foreach (array_keys($this->cache) as $key) {
×
142
            if (fnmatch($pattern, $key)) {
×
143
                $count++;
×
144
                unset($this->cache[$key], $this->expirations[$key]);
×
145
            }
146
        }
147

148
        return $count;
×
149
    }
150

151
    /**
152
     * Performs atomic incrementation of a raw stored value.
153
     */
154
    public function increment(string $key, int $offset = 1): bool
155
    {
156
        $key  = static::validateKey($key, $this->prefix);
×
157
        $data = $this->cache[$key] ?: null;
×
158

159
        if ($data === null) {
×
160
            $data = 0;
×
161
        } elseif (! is_int($data)) {
×
162
            return false;
×
163
        }
164

165
        return $this->save($key, $data + $offset);
×
166
    }
167

168
    /**
169
     * Performs atomic decrementation of a raw stored value.
170
     */
171
    public function decrement(string $key, int $offset = 1): bool
172
    {
173
        $key = static::validateKey($key, $this->prefix);
×
174

175
        $data = $this->cache[$key] ?: null;
×
176

177
        if ($data === null) {
×
178
            $data = 0;
×
179
        } elseif (! is_int($data)) {
×
180
            return false;
×
181
        }
182

183
        return $this->save($key, $data - $offset);
×
184
    }
185

186
    /**
187
     * Will delete all items in the entire cache.
188
     */
189
    public function clean(): true
190
    {
191
        $this->cache       = [];
12✔
192
        $this->expirations = [];
12✔
193
        $this->lockStore?->clean();
12✔
194

195
        return true;
12✔
196
    }
197

198
    /**
199
     * Returns information on the entire cache.
200
     *
201
     * The information returned and the structure of the data
202
     * varies depending on the handler.
203
     *
204
     * @return list<string> Keys currently present in the store
205
     */
206
    public function getCacheInfo(): array
207
    {
208
        return array_keys($this->cache);
×
209
    }
210

211
    /**
212
     * Returns detailed information about the specific item in the cache.
213
     *
214
     * @return array{expire: int|null}|null Returns null if the item does not exist,
215
     *                                      otherwise, array with the 'expire' key for
216
     *                                      absolute epoch expiry (or null).
217
     */
218
    public function getMetaData(string $key): ?array
219
    {
220
        // Misses return null
221
        if (! array_key_exists($key, $this->expirations)) {
×
222
            return null;
×
223
        }
224

225
        // Count expired items as a miss
226
        if (is_int($this->expirations[$key]) && $this->expirations[$key] > Time::now()->getTimestamp()) {
×
227
            return null;
×
228
        }
229

230
        return ['expire' => $this->expirations[$key]];
×
231
    }
232

233
    /**
234
     * Determine if the driver is supported on this system.
235
     */
236
    public function isSupported(): bool
237
    {
238
        return true;
×
239
    }
240

241
    public function lockStore(): LockStoreInterface
242
    {
243
        return $this->lockStore ??= new MockLockStore();
2✔
244
    }
245

246
    // --------------------------------------------------------------------
247
    // Test Helpers
248
    // --------------------------------------------------------------------
249

250
    /**
251
     * Instructs the class to ignore all
252
     * requests to cache an item, and always "miss"
253
     * when checked for existing data.
254
     *
255
     * @return $this
256
     */
257
    public function bypass(bool $bypass = true)
258
    {
259
        $this->clean();
1✔
260
        $this->bypass = $bypass;
1✔
261

262
        return $this;
1✔
263
    }
264

265
    // --------------------------------------------------------------------
266
    // Additional Assertions
267
    // --------------------------------------------------------------------
268

269
    /**
270
     * Asserts that the cache has an item named $key.
271
     * The value is not checked since storing false or null
272
     * values is valid.
273
     *
274
     * @return void
275
     */
276
    public function assertHas(string $key)
277
    {
278
        Assert::assertNotNull($this->get($key), "The cache does not have an item named: `{$key}`");
1✔
279
    }
280

281
    /**
282
     * Asserts that the cache has an item named $key with a value matching $value.
283
     *
284
     * @param mixed $value
285
     *
286
     * @return void
287
     */
288
    public function assertHasValue(string $key, $value = null)
289
    {
290
        $item = $this->get($key);
1✔
291

292
        // Let assertHas() handle throwing the error for consistency
293
        // if the key is not found
294
        if ($item === null) {
1✔
295
            $this->assertHas($key);
×
296
        }
297

298
        Assert::assertSame($value, $this->get($key), "The cached item `{$key}` does not equal match expectation. Found: " . print_r($value, true));
1✔
299
    }
300

301
    /**
302
     * Asserts that the cache does NOT have an item named $key.
303
     *
304
     * @return void
305
     */
306
    public function assertMissing(string $key)
307
    {
308
        Assert::assertArrayNotHasKey($key, $this->cache, "The cached item named `{$key}` exists.");
1✔
309
    }
310
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc