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

aplus-framework / cache / 15497987025

06 Jun 2025 07:12PM UTC coverage: 94.813% (-0.4%) from 95.228%
15497987025

push

github

natanfelles
Update chmod

457 of 482 relevant lines covered (94.81%)

51.84 hits per line

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

85.53
/src/FilesCache.php
1
<?php declare(strict_types=1);
2
/*
3
 * This file is part of Aplus Framework Cache Library.
4
 *
5
 * (c) Natan Felles <natanfelles@gmail.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace Framework\Cache;
11

12
use Framework\Log\Logger;
13
use InvalidArgumentException;
14
use JetBrains\PhpStorm\Pure;
15
use RuntimeException;
16
use SensitiveParameter;
17

18
/**
19
 * Class FilesCache.
20
 *
21
 * @package cache
22
 */
23
class FilesCache extends Cache
24
{
25
    /**
26
     * Files Cache handler configurations.
27
     *
28
     * @var array<string,mixed>
29
     */
30
    protected array $configs = [
31
        'directory' => null,
32
        'files_permission' => 0644,
33
        'gc' => 1,
34
    ];
35
    /**
36
     * @var string|null
37
     */
38
    protected ?string $baseDirectory;
39

40
    /**
41
     * FilesCache constructor.
42
     *
43
     * @param array<string,mixed>|null $configs Driver specific configurations
44
     * @param string|null $prefix Keys prefix
45
     * @param Serializer|string $serializer Data serializer
46
     * @param Logger|null $logger Logger instance
47
     */
48
    public function __construct(
49
        #[SensitiveParameter]
50
        ?array $configs = [],
51
        ?string $prefix = null,
52
        Serializer | string $serializer = Serializer::PHP,
53
        ?Logger $logger = null
54
    ) {
55
        parent::__construct($configs, $prefix, $serializer, $logger);
120✔
56
    }
57

58
    public function __destruct()
59
    {
60
        if (\rand(1, 100) <= $this->configs['gc']) {
120✔
61
            $this->gc();
120✔
62
        }
63
        parent::__destruct();
120✔
64
    }
65

66
    protected function initialize() : void
67
    {
68
        $this->setBaseDirectory();
120✔
69
        $this->setGC($this->configs['gc']);
120✔
70
    }
71

72
    protected function setGC(int $gc) : void
73
    {
74
        if ($gc < 1 || $gc > 100) {
120✔
75
            throw new InvalidArgumentException(
5✔
76
                "Invalid cache GC: {$gc}"
5✔
77
            );
5✔
78
        }
79
    }
80

81
    protected function setBaseDirectory() : void
82
    {
83
        $path = $this->configs['directory'];
120✔
84
        if ($path === null) {
120✔
85
            $path = \sys_get_temp_dir();
5✔
86
        }
87
        $real = \realpath($path);
120✔
88
        if ($real === false) {
120✔
89
            throw new RuntimeException("Invalid cache directory: {$path}");
5✔
90
        }
91
        $real = \rtrim($path, \DIRECTORY_SEPARATOR) . \DIRECTORY_SEPARATOR;
120✔
92
        if (isset($this->prefix[0])) {
120✔
93
            $real .= $this->prefix;
120✔
94
        }
95
        if (!\is_dir($real)) {
120✔
96
            throw new RuntimeException(
5✔
97
                "Invalid cache directory path: {$real}"
5✔
98
            );
5✔
99
        }
100
        if (!\is_writable($real)) {
120✔
101
            throw new RuntimeException(
5✔
102
                "Cache directory is not writable: {$real}"
5✔
103
            );
5✔
104
        }
105
        $this->baseDirectory = $real . \DIRECTORY_SEPARATOR;
120✔
106
    }
107

108
    public function get(string $key) : mixed
109
    {
110
        if (isset($this->debugCollector)) {
70✔
111
            $start = \microtime(true);
10✔
112
            return $this->addDebugGet(
10✔
113
                $key,
10✔
114
                $start,
10✔
115
                $this->getContents($this->renderFilepath($key))
10✔
116
            );
10✔
117
        }
118
        return $this->getContents($this->renderFilepath($key));
60✔
119
    }
120

121
    /**
122
     * @param string $filepath
123
     *
124
     * @return mixed
125
     */
126
    protected function getContents(string $filepath) : mixed
127
    {
128
        if (!\is_file($filepath)) {
70✔
129
            return null;
65✔
130
        }
131
        $value = @\file_get_contents($filepath);
60✔
132
        if ($value === false) {
60✔
133
            $this->log("Cache (files): File '{$filepath}' could not be read");
×
134
            return null;
×
135
        }
136
        $value = (array) $this->unserialize($value);
60✔
137
        if (!isset($value['ttl'], $value['data']) || $value['ttl'] <= \time()) {
60✔
138
            $this->deleteFile($filepath);
35✔
139
            return null;
35✔
140
        }
141
        return $value['data'];
55✔
142
    }
143

144
    protected function createSubDirectory(string $filepath) : void
145
    {
146
        $dirname = \dirname($filepath);
70✔
147
        if (\is_dir($dirname)) {
70✔
148
            return;
20✔
149
        }
150
        if (!\mkdir($dirname, 0777, true) || !\is_dir($dirname)) {
70✔
151
            throw new RuntimeException(
×
152
                "Directory key was not created: {$filepath}"
×
153
            );
×
154
        }
155
    }
156

157
    public function set(string $key, mixed $value, ?int $ttl = null) : bool
158
    {
159
        if (isset($this->debugCollector)) {
70✔
160
            $start = \microtime(true);
5✔
161
            return $this->addDebugSet(
5✔
162
                $key,
5✔
163
                $ttl,
5✔
164
                $start,
5✔
165
                $value,
5✔
166
                $this->setValue($key, $value, $ttl)
5✔
167
            );
5✔
168
        }
169
        return $this->setValue($key, $value, $ttl);
65✔
170
    }
171

172
    public function setValue(string $key, mixed $value, ?int $ttl = null) : bool
173
    {
174
        $filepath = $this->renderFilepath($key);
70✔
175
        $this->createSubDirectory($filepath);
70✔
176
        $value = [
70✔
177
            'ttl' => \time() + $this->makeTtl($ttl),
70✔
178
            'data' => $value,
70✔
179
        ];
70✔
180
        $value = $this->serialize($value);
70✔
181
        $isFile = \is_file($filepath);
70✔
182
        $written = @\file_put_contents($filepath, $value, \LOCK_EX);
70✔
183
        if ($written !== false && $isFile === false) {
70✔
184
            \chmod($filepath, $this->configs['files_permission']);
70✔
185
        }
186
        if ($written === false) {
70✔
187
            $this->log("Cache (files): File '{$filepath}' could not be written");
5✔
188
            return false;
5✔
189
        }
190
        return true;
70✔
191
    }
192

193
    public function delete(string $key) : bool
194
    {
195
        if (isset($this->debugCollector)) {
15✔
196
            $start = \microtime(true);
5✔
197
            return $this->addDebugDelete(
5✔
198
                $key,
5✔
199
                $start,
5✔
200
                $this->deleteFile($this->renderFilepath($key))
5✔
201
            );
5✔
202
        }
203
        return $this->deleteFile($this->renderFilepath($key));
10✔
204
    }
205

206
    public function flush() : bool
207
    {
208
        if (isset($this->debugCollector)) {
120✔
209
            $start = \microtime(true);
20✔
210
            return $this->addDebugFlush(
20✔
211
                $start,
20✔
212
                $this->deleteAll($this->baseDirectory)
20✔
213
            );
20✔
214
        }
215
        return $this->deleteAll($this->baseDirectory);
100✔
216
    }
217

218
    /**
219
     * Garbage collector.
220
     *
221
     * Deletes all expired items.
222
     *
223
     * @return bool TRUE if all expired items was deleted, FALSE if a fail occurs
224
     */
225
    public function gc() : bool
226
    {
227
        return $this->deleteExpired($this->baseDirectory);
120✔
228
    }
229

230
    protected function deleteExpired(string $baseDirectory) : bool
231
    {
232
        $handle = $this->openDir($baseDirectory);
120✔
233
        if ($handle === false) {
120✔
234
            return false;
×
235
        }
236
        $baseDirectory = \rtrim($baseDirectory, \DIRECTORY_SEPARATOR) . \DIRECTORY_SEPARATOR;
120✔
237
        $status = true;
120✔
238
        while (($path = \readdir($handle)) !== false) {
120✔
239
            if ($path[0] === '.') {
120✔
240
                continue;
120✔
241
            }
242
            $path = $baseDirectory . $path;
5✔
243
            if (\is_file($path)) {
5✔
244
                $this->getContents($path);
5✔
245
                continue;
5✔
246
            }
247
            if (!$this->deleteExpired($path)) {
5✔
248
                $status = false;
×
249
                break;
×
250
            }
251
            if (\scandir($path, \SCANDIR_SORT_ASCENDING) === ['.', '..'] && !\rmdir($path)) {
5✔
252
                $status = false;
×
253
                break;
×
254
            }
255
        }
256
        $this->closeDir($handle);
120✔
257
        return $status;
120✔
258
    }
259

260
    protected function deleteAll(string $baseDirectory) : bool
261
    {
262
        $handle = $this->openDir($baseDirectory);
120✔
263
        if ($handle === false) {
120✔
264
            return false;
×
265
        }
266
        $baseDirectory = \rtrim($baseDirectory, \DIRECTORY_SEPARATOR) . \DIRECTORY_SEPARATOR;
120✔
267
        $status = true;
120✔
268
        while (($path = \readdir($handle)) !== false) {
120✔
269
            if ($path[0] === '.') {
120✔
270
                continue;
120✔
271
            }
272
            $path = $baseDirectory . $path;
70✔
273
            if (\is_file($path)) {
70✔
274
                if (\unlink($path)) {
40✔
275
                    continue;
40✔
276
                }
277
                $this->log("Cache (files): File '{$path}' could not be deleted");
×
278
                $status = false;
×
279
                break;
×
280
            }
281
            if (!$this->deleteAll($path)) {
70✔
282
                $status = false;
×
283
                break;
×
284
            }
285
            if (\scandir($path, \SCANDIR_SORT_ASCENDING) === ['.', '..'] && !\rmdir($path)) {
70✔
286
                $status = false;
×
287
                break;
×
288
            }
289
        }
290
        $this->closeDir($handle);
120✔
291
        return $status;
120✔
292
    }
293

294
    protected function deleteFile(string $filepath) : bool
295
    {
296
        if (\is_file($filepath)) {
50✔
297
            $deleted = \unlink($filepath);
50✔
298
            if ($deleted === false) {
50✔
299
                $this->log("Cache (files): File '{$filepath}' could not be deleted");
×
300
                return false;
×
301
            }
302
        }
303
        return true;
50✔
304
    }
305

306
    /**
307
     * @param string $dirpath
308
     *
309
     * @return false|resource
310
     */
311
    protected function openDir(string $dirpath)
312
    {
313
        $real = \realpath($dirpath);
120✔
314
        if ($real === false) {
120✔
315
            return false;
×
316
        }
317
        if (!\is_dir($real)) {
120✔
318
            return false;
×
319
        }
320
        $real = \rtrim($real, \DIRECTORY_SEPARATOR) . \DIRECTORY_SEPARATOR;
120✔
321
        if (!\str_starts_with($real, $this->configs['directory'])) {
120✔
322
            return false;
×
323
        }
324
        return \opendir($real);
120✔
325
    }
326

327
    /**
328
     * @param resource $resource
329
     */
330
    protected function closeDir($resource) : void
331
    {
332
        if (\is_resource($resource)) {
120✔
333
            \closedir($resource);
120✔
334
        }
335
    }
336

337
    #[Pure]
338
    protected function renderFilepath(string $key) : string
339
    {
340
        $key = \md5($key);
75✔
341
        return $this->baseDirectory .
75✔
342
            $key[0] . $key[1] . \DIRECTORY_SEPARATOR .
75✔
343
            $key;
75✔
344
    }
345
}
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