Passed
Push — redis ( 69cfbf...5e7e33 )
by Francis
10:01
created

TempFileCache::__construct()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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