TempFileCache::clean()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.25

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
c 1
b 0
f 0
dl 0
loc 18
ccs 6
cts 8
cp 0.75
rs 9.9666
cc 4
nc 4
nop 0
crap 4.25
1
<?php
2
/** @noinspection PhpUsageOfSilenceOperatorInspection */
3
4
namespace Vectorface\Cache;
5
6
use DateInterval;
7
use Exception;
8
use Psr\SimpleCache\InvalidArgumentException;
9
use Vectorface\Cache\Common\MultipleTrait;
10
use Vectorface\Cache\Common\PSR16Util;
11
12
/**
13
 * Represents a cache whose entries are stored in temporary files.
14
 */
15
class TempFileCache implements Cache
16
{
17
    use MultipleTrait, PSR16Util;
18
19
    private string $directory;
20
    private string $extension;
21
22
    /**
23
     * Create a temporary file cache.
24
     *
25
     * @param string|null $directory The directory in which cache files should be stored.
26
     * @param string $extension The extension to be used after the filename to uniquely identify cache files.
27
     *
28
     * Note:
29
     *  - Without a directory argument, the system tempdir will be used (e.g. /tmp/TempFileCache/)
30
     *  - If given a relative path, it will create that directory within the system tempdir.
31
     *  - If given an absolute path, it will attempt to use that path as-is. Not recommended.
32
     * @throws Exception
33
     */
34
    public function __construct(string|null $directory = null, string $extension = '.tempcache')
35
    {
36
        $this->directory = $this->getTempDir($directory);
37
        $this->checkAndCreateDir($this->directory);
38
39
        $realpath = realpath($this->directory); /* Get rid of extraneous symlinks, ..'s, etc. */
40 23
        if (!$realpath) {
41
            // @codeCoverageIgnoreStart
42 23
            throw new Exception("Could not get directory realpath");
43 23
            // @codeCoverageIgnoreEnd
44
        }
45 23
        $this->directory = $realpath;
46 23
47
        $this->extension = empty($extension) ? "" : $extension;
48
    }
49 23
50
    /**
51 23
     * Check for a directory's existence and writability, and create otherwise
52 23
     *
53
     * @param string $directory
54
     * @throws Exception
55
     */
56
    private function checkAndCreateDir(string $directory) : void
57
    {
58
        if (!file_exists($directory)) {
59
            if (!@mkdir($directory, 0700, true)) {
60 23
                throw new Exception("Directory does not exist, and could not be created: {$directory}");
61
            }
62 23
        } elseif (is_dir($directory)) {
63 23
            if (!is_writable($directory)) {
64 23
                throw new Exception("Directory is not writable: {$directory}");
65
            }
66 3
        } else {
67 3
            throw new Exception("Not a directory: {$directory}");
68 3
        }
69
    }
70
71 1
    /**
72
     * Generate a consistent temporary directory based on a requested directory name.
73 23
     *
74
     * @param string|null $directory The name or path of a temporary directory.
75
     * @return string The directory name, resolved to a full path.
76
     */
77
    private function getTempDir(string|null $directory) : string
78
    {
79
        if (empty($directory)) {
80
            $classParts = explode("\\", static::class);
81 23
            return sys_get_temp_dir()  . '/' . end($classParts);
82
        }
83 23
        if (!str_starts_with($directory, '/')) {
84 23
            return sys_get_temp_dir() . '/' . $directory;
85 23
        }
86 2
        return $directory;
87 1
    }
88
89 1
    /**
90
     * @inheritDoc
91
     */
92
    public function get(string $key, mixed $default = null) : mixed
93
    {
94
        $file = $this->makePath($this->key($key));
95
        $data = @file_get_contents($file);
96 20
        if (!$data) {
97
            return $default;
98 20
        }
99 19
100 19
        $data = @unserialize($data);
101 16
        if (!$data) {
102
            $this->delete($key); /* Delete corrupted. */
103
            return $default;
104 9
        }
105 9
106 1
        [$expiry, $value] = $data;
107 1
        if ($expiry !== false && ($expiry < microtime(true))) {
108
            $this->delete($key);
109
            return $default;
110 8
        }
111 8
112 1
        return $value;
113 1
    }
114
115
    /**
116 8
     * @inheritDoc
117
     */
118
    public function set(string $key, mixed $value, DateInterval|int|null $ttl = null) : bool
119
    {
120
        $ttl = $this->ttl($ttl);
121
        $data = [$ttl ? microtime(true) + $ttl : false, $value];
122 20
        return @file_put_contents($this->makePath($this->key($key)), serialize($data)) !== false;
123
    }
124 20
125 20
    /**
126 20
     * @inheritDoc
127
     */
128
    public function delete(string $key) : bool
129
    {
130
        return @unlink($this->makePath($this->key($key)));
131
    }
132 7
133
    /**
134 7
     * @inheritDoc
135
     */
136
    public function clean() : bool
137
    {
138
        if (!($files = $this->getCacheFiles())) {
139
            return false;
140 2
        }
141
142 2
        foreach ($files as $file) {
143 1
            $key = basename($file, $this->extension);
144
            try {
145
                // Automatically deletes if expired
146 1
                $this->get($key);
147 1
                // @codeCoverageIgnoreStart
148
            } catch (InvalidArgumentException) {
149
                return false;
150 1
                // @codeCoverageIgnoreEnd
151
            }
152
        }
153
        return true;
154
    }
155 1
156
    /**
157
     * @inheritDoc
158
     */
159
    public function flush() : bool
160
    {
161 23
        if (($files = $this->getCacheFiles()) === false) {
162
            return false;
163 23
        }
164 1
165
        $result = true;
166
        foreach ($files as $file) {
167 23
            $result = $result && @unlink($file);
168 23
        }
169 12
        return $result;
170
    }
171 23
172
    /**
173
     * @inheritDoc
174
     */
175
    public function clear() : bool
176
    {
177 3
        return $this->flush();
178
    }
179 3
180
    /**
181
     * @inheritDoc
182
     */
183
    public function has(string $key) : bool
184
    {
185 1
        return $this->get($key) !== null;
186
    }
187 1
188
    /**
189
     * Creates a file path in the form directory/key.extension
190
     *
191
     * @param  string $key the key of the cached element
192
     * @return string The file path to the cached element's enclosing file.
193
     */
194
    private function makePath(string $key) : string
195
    {
196 19
        return $this->directory . "/" . hash("sha224", $key) . $this->extension;
197
    }
198 19
199
    /**
200
     * Finds all files with the cache extension in the cache directory
201
     *
202
     * @return array|false Returns an array of filenames that represent cached entries.
203
     */
204
    private function getCacheFiles() : array|false
205
    {
206 23
        if (!($files = @scandir($this->directory, 1))) {
207
            return false;
208 23
        }
209 1
210
        $negExtLen = -1 * strlen($this->extension);
211
        $return = [];
212 23
        foreach ($files as $file) {
213 23
            if (substr($file, $negExtLen) === $this->extension) {
214 23
                $return[] = $this->directory . '/' . $file;
215 23
            }
216 12
        }
217
        return $return;
218
    }
219 23
220
    /**
221
     * Destroy this cache; Clear everything.
222
     *
223
     * Any operations on the cache after this operation are invalid, and their behavior will be undefined.
224
     *
225
     * @return bool True if the cache was flushed and the directory deleted.
226
     */
227
    public function destroy() : bool
228
    {
229 23
        return $this->flush() && @rmdir($this->directory);
230
    }
231
}
232