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

nette / caching / 22226743054

20 Feb 2026 01:53PM UTC coverage: 88.227% (+1.0%) from 87.202%
22226743054

Pull #85

github

web-flow
Merge c3f6b576c into f6698ea58
Pull Request #85: Extend MemoryStorage with Tags, Priority, Expire and Sliding modifiers

35 of 36 new or added lines in 1 file covered. (97.22%)

57 existing lines in 7 files now uncovered.

622 of 705 relevant lines covered (88.23%)

0.88 hits per line

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

80.84
/src/Caching/Storages/FileStorage.php
1
<?php
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Nette\Caching\Storages;
11

12
use Nette;
13
use Nette\Caching\Cache;
14
use function dirname, fclose, filemtime, flock, fopen, fseek, ftruncate, fwrite, is_dir, is_string, microtime, mkdir, mt_getrandmax, mt_rand, rmdir, serialize, str_pad, str_repeat, stream_get_contents, strlen, strrpos, substr_replace, time, touch, unlink, unserialize, urlencode;
15
use const LOCK_EX, LOCK_SH, LOCK_UN, STR_PAD_LEFT;
16

17

18
/**
19
 * Cache file storage.
20
 */
21
class FileStorage implements Nette\Caching\Storage
22
{
23
        /**
24
         * Atomic thread safe logic:
25
         *
26
         * 1) reading: open(r+b), lock(SH), read
27
         *     - delete?: delete*, close
28
         * 2) deleting: delete*
29
         * 3) writing: open(r+b || wb), lock(EX), truncate*, write data, write meta, close
30
         *
31
         * delete* = try unlink, if fails (on NTFS) { lock(EX), truncate, close, unlink } else close (on ext3)
32
         */
33

34
        /** @internal cache file structure: meta-struct size + serialized meta-struct + data */
35
        private const
36
                MetaHeaderLen = 6,
37
                // meta structure: array of
38
                MetaTime = 'time', // timestamp
39
                MetaSerialized = 'serialized', // is content serialized?
40
                MetaExpire = 'expire', // expiration timestamp
41
                MetaDelta = 'delta', // relative (sliding) expiration
42
                MetaItems = 'di', // array of dependent items (file => timestamp)
43
                MetaCallbacks = 'callbacks'; // array of callbacks (function, args)
44

45
        /** additional cache structure */
46
        private const
47
                File = 'file',
48
                Handle = 'handle';
49

50
        /** probability that the clean() routine is started */
51
        public static float $gcProbability = 0.001;
52

53
        private string $dir;
54
        private ?Journal $journal;
55

56
        /** @var array<string, resource>  key => file handle */
57
        private array $locks;
58

59

60
        public function __construct(string $dir, ?Journal $journal = null)
1✔
61
        {
62
                if (!is_dir($dir) || !Nette\Utils\FileSystem::isAbsolute($dir)) {
1✔
63
                        throw new Nette\DirectoryNotFoundException("Directory '$dir' not found or is not absolute.");
1✔
64
                }
65

66
                $this->dir = $dir;
1✔
67
                $this->journal = $journal;
1✔
68

69
                if (mt_rand() / mt_getrandmax() < static::$gcProbability) {
1✔
UNCOV
70
                        $this->clean([]);
×
71
                }
72
        }
1✔
73

74

75
        public function read(string $key): mixed
1✔
76
        {
77
                $meta = $this->readMetaAndLock($this->getCacheFile($key), LOCK_SH);
1✔
78
                return $meta && $this->verify($meta)
1✔
79
                        ? $this->readData($meta) // calls fclose()
1✔
80
                        : null;
1✔
81
        }
82

83

84
        /**
85
         * Verifies dependencies.
86
         * @param  array<string, mixed>  $meta
87
         */
88
        private function verify(array $meta): bool
1✔
89
        {
90
                do {
91
                        if (!empty($meta[self::MetaDelta])) {
1✔
92
                                // meta[file] was added by readMetaAndLock()
93
                                if (filemtime($meta[self::File]) + $meta[self::MetaDelta] < time()) {
1✔
94
                                        break;
1✔
95
                                }
96

97
                                touch($meta[self::File]);
1✔
98

99
                        } elseif (!empty($meta[self::MetaExpire]) && $meta[self::MetaExpire] < time()) {
1✔
100
                                break;
1✔
101
                        }
102

103
                        if (!empty($meta[self::MetaCallbacks]) && !Cache::checkCallbacks($meta[self::MetaCallbacks])) {
1✔
104
                                break;
1✔
105
                        }
106

107
                        if (!empty($meta[self::MetaItems])) {
1✔
108
                                foreach ($meta[self::MetaItems] as $depFile => $time) {
1✔
109
                                        $m = $this->readMetaAndLock($depFile, LOCK_SH);
1✔
110
                                        if (($m[self::MetaTime] ?? null) !== $time || ($m && !$this->verify($m))) {
1✔
111
                                                break 2;
1✔
112
                                        }
113
                                }
114
                        }
115

116
                        return true;
1✔
UNCOV
117
                } while (false);
×
118

119
                $this->delete($meta[self::File], $meta[self::Handle]); // meta[handle] & meta[file] was added by readMetaAndLock()
1✔
120
                return false;
1✔
121
        }
122

123

124
        public function lock(string $key): void
1✔
125
        {
126
                $cacheFile = $this->getCacheFile($key);
1✔
127
                if (!is_dir($dir = dirname($cacheFile))) {
1✔
128
                        @mkdir($dir); // @ - directory may already exist
1✔
129
                }
130

131
                $handle = fopen($cacheFile, 'c+b');
1✔
132
                if (!$handle) {
1✔
UNCOV
133
                        return;
×
134
                }
135

136
                $this->locks[$key] = $handle;
1✔
137
                flock($handle, LOCK_EX);
1✔
138
        }
1✔
139

140

141
        /** @param  array<string, mixed>  $dp */
142
        public function write(string $key, $data, array $dp): void
1✔
143
        {
144
                $meta = [
1✔
145
                        self::MetaTime => microtime(),
1✔
146
                ];
147

148
                if (isset($dp[Cache::Expire])) {
1✔
149
                        if (empty($dp[Cache::Sliding])) {
1✔
150
                                $meta[self::MetaExpire] = $dp[Cache::Expire] + time(); // absolute time
1✔
151
                        } else {
152
                                $meta[self::MetaDelta] = (int) $dp[Cache::Expire]; // sliding time
1✔
153
                        }
154
                }
155

156
                if (isset($dp[Cache::Items])) {
1✔
157
                        foreach ($dp[Cache::Items] as $item) {
1✔
158
                                $depFile = $this->getCacheFile($item);
1✔
159
                                $m = $this->readMetaAndLock($depFile, LOCK_SH);
1✔
160
                                $meta[self::MetaItems][$depFile] = $m[self::MetaTime] ?? null;
1✔
161
                                unset($m);
1✔
162
                        }
163
                }
164

165
                if (isset($dp[Cache::Callbacks])) {
1✔
166
                        $meta[self::MetaCallbacks] = $dp[Cache::Callbacks];
1✔
167
                }
168

169
                if (!isset($this->locks[$key])) {
1✔
170
                        $this->lock($key);
1✔
171
                        if (!isset($this->locks[$key])) {
1✔
UNCOV
172
                                return;
×
173
                        }
174
                }
175

176
                $handle = $this->locks[$key];
1✔
177
                unset($this->locks[$key]);
1✔
178

179
                $cacheFile = $this->getCacheFile($key);
1✔
180

181
                if (isset($dp[Cache::Tags]) || isset($dp[Cache::Priority])) {
1✔
182
                        if (!$this->journal) {
1✔
183
                                throw new Nette\InvalidStateException('CacheJournal has not been provided.');
1✔
184
                        }
185

186
                        $this->journal->write($cacheFile, $dp);
1✔
187
                }
188

189
                ftruncate($handle, 0);
1✔
190

191
                if (!is_string($data)) {
1✔
192
                        $data = serialize($data);
1✔
193
                        $meta[self::MetaSerialized] = true;
1✔
194
                }
195

196
                $head = serialize($meta);
1✔
197
                $head = str_pad((string) strlen($head), 6, '0', STR_PAD_LEFT) . $head;
1✔
198
                $headLen = strlen($head);
1✔
199

200
                do {
201
                        if (fwrite($handle, str_repeat("\x00", $headLen)) !== $headLen) {
1✔
UNCOV
202
                                break;
×
203
                        }
204

205
                        if (fwrite($handle, $data) !== strlen($data)) {
1✔
206
                                break;
×
207
                        }
208

209
                        fseek($handle, 0);
1✔
210
                        if (fwrite($handle, $head) !== $headLen) {
1✔
UNCOV
211
                                break;
×
212
                        }
213

214
                        flock($handle, LOCK_UN);
1✔
215
                        fclose($handle);
1✔
216
                        return;
1✔
UNCOV
217
                } while (false);
×
218

UNCOV
219
                $this->delete($cacheFile, $handle);
×
220
        }
221

222

223
        public function remove(string $key): void
1✔
224
        {
225
                unset($this->locks[$key]);
1✔
226
                $this->delete($this->getCacheFile($key));
1✔
227
        }
1✔
228

229

230
        /** @param  array<string, mixed>  $conditions */
231
        public function clean(array $conditions): void
1✔
232
        {
233
                $all = !empty($conditions[Cache::All]);
1✔
234
                $collector = empty($conditions);
1✔
235
                $namespaces = $conditions[Cache::Namespaces] ?? null;
1✔
236

237
                // cleaning using file iterator
238
                if ($all || $collector) {
1✔
239
                        $now = time();
1✔
240
                        foreach (Nette\Utils\Finder::find('_*')->from($this->dir)->childFirst() as $entry) {
1✔
241
                                $path = (string) $entry;
1✔
242
                                if ($entry->isDir()) { // collector: remove empty dirs
1✔
243
                                        @rmdir($path); // @ - removing dirs is not necessary
1✔
244
                                        continue;
1✔
245
                                }
246

247
                                if ($all) {
1✔
248
                                        $this->delete($path);
1✔
249

250
                                } else { // collector
UNCOV
251
                                        $meta = $this->readMetaAndLock($path, LOCK_SH);
×
UNCOV
252
                                        if (!$meta) {
×
UNCOV
253
                                                continue;
×
254
                                        }
255

256
                                        if ((!empty($meta[self::MetaDelta]) && filemtime($meta[self::File]) + $meta[self::MetaDelta] < $now)
×
257
                                                || (!empty($meta[self::MetaExpire]) && $meta[self::MetaExpire] < $now)
×
258
                                        ) {
UNCOV
259
                                                $this->delete($path, $meta[self::Handle]);
×
UNCOV
260
                                                continue;
×
261
                                        }
262

UNCOV
263
                                        flock($meta[self::Handle], LOCK_UN);
×
264
                                        fclose($meta[self::Handle]);
×
265
                                }
266
                        }
267

268
                        if ($this->journal) {
1✔
269
                                $this->journal->clean($conditions);
×
270
                        }
271

272
                        return;
1✔
273

274
                } elseif ($namespaces) {
1✔
275
                        foreach ($namespaces as $namespace) {
1✔
276
                                $dir = $this->dir . '/_' . urlencode($namespace);
1✔
277
                                if (!is_dir($dir)) {
1✔
UNCOV
278
                                        continue;
×
279
                                }
280

281
                                foreach (Nette\Utils\Finder::findFiles('_*')->in($dir) as $entry) {
1✔
282
                                        $this->delete((string) $entry);
1✔
283
                                }
284

285
                                @rmdir($dir); // may already contain new files
1✔
286
                        }
287
                }
288

289
                // cleaning using journal
290
                if ($this->journal) {
1✔
291
                        foreach ($this->journal->clean($conditions) as $file) {
1✔
292
                                $this->delete($file);
1✔
293
                        }
294
                }
295
        }
1✔
296

297

298
        /**
299
         * Reads cache data from disk.
300
         * @return array<string, mixed>|null  meta data with 'file' and 'handle' keys added, or null if not found
301
         */
302
        protected function readMetaAndLock(string $file, int $lock): ?array
1✔
303
        {
304
                $handle = @fopen($file, 'r+b'); // @ - file may not exist
1✔
305
                if (!$handle) {
1✔
306
                        return null;
1✔
307
                }
308

309
                flock($handle, $lock);
1✔
310

311
                $size = (int) stream_get_contents($handle, self::MetaHeaderLen);
1✔
312
                if ($size) {
1✔
313
                        $meta = stream_get_contents($handle, $size, self::MetaHeaderLen);
1✔
314
                        $meta = unserialize($meta);
1✔
315
                        $meta[self::File] = $file;
1✔
316
                        $meta[self::Handle] = $handle;
1✔
317
                        return $meta;
1✔
318
                }
319

UNCOV
320
                flock($handle, LOCK_UN);
×
UNCOV
321
                fclose($handle);
×
UNCOV
322
                return null;
×
323
        }
324

325

326
        /**
327
         * Reads cache data from disk and closes cache file handle.
328
         * @param  array<string, mixed>  $meta
329
         */
330
        protected function readData(array $meta): mixed
1✔
331
        {
332
                $data = stream_get_contents($meta[self::Handle]);
1✔
333
                flock($meta[self::Handle], LOCK_UN);
1✔
334
                fclose($meta[self::Handle]);
1✔
335

336
                return empty($meta[self::MetaSerialized]) ? $data : unserialize($data);
1✔
337
        }
338

339

340
        /**
341
         * Returns file name.
342
         */
343
        protected function getCacheFile(string $key): string
1✔
344
        {
345
                $file = urlencode($key);
1✔
346
                if ($a = strrpos($file, '%00')) { // %00 = urlencode(Nette\Caching\Cache::NamespaceSeparator)
1✔
347
                        $file = substr_replace($file, '/_', $a, 3);
1✔
348
                }
349

350
                return $this->dir . '/_' . $file;
1✔
351
        }
352

353

354
        /**
355
         * Deletes and closes file.
356
         * @param  resource  $handle
357
         */
358
        private static function delete(string $file, $handle = null): void
1✔
359
        {
360
                if (@unlink($file)) { // @ - file may not already exist
1✔
361
                        if ($handle) {
1✔
362
                                flock($handle, LOCK_UN);
1✔
363
                                fclose($handle);
1✔
364
                        }
365

366
                        return;
1✔
367
                }
368

UNCOV
369
                if (!$handle) {
×
UNCOV
370
                        $handle = @fopen($file, 'r+'); // @ - file may not exist
×
371
                }
372

UNCOV
373
                if (!$handle) {
×
UNCOV
374
                        return;
×
375
                }
376

377
                flock($handle, LOCK_EX);
×
UNCOV
378
                ftruncate($handle, 0);
×
UNCOV
379
                flock($handle, LOCK_UN);
×
380
                fclose($handle);
×
381
                @unlink($file); // @ - file may not already exist
×
382
        }
383
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc