Completed
Push — master ( 7514f5...f083aa )
by Nielsen
01:57
created

FileCache::streamSafeGlob()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 9
cts 9
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 9
nc 4
nop 2
crap 4
1
<?php
2
3
namespace Ezcache\Cache;
4
5
use DateInterval;
6
use DateTime;
7
use Exception;
8
9
/**
10
 * Class FileCache
11
 *
12
 * @package Ezcache\Cache
13
 * @author nielsengoncalves
14
 */
15
class FileCache implements CacheInterface {
16
17
    const JSON_FORMAT = '.json';
18
19
    private $cacheDirectory;
20
    private $ttl;
21
    private $lastError = [];
22
    private $namespace;
23
24
    /**
25
     * FileCache constructor.
26
     *
27
     * @param string $directory the directory where cache operations will happen.
28
     * @param int $ttl the cache life time in seconds (0 = Forever).
29
     * @param string $namespace the cache namespace.
30
     */
31 6
    public function __construct(string $directory, int $ttl = 0, string $namespace = null) {
32 6
        $this->setCacheDirectory($directory);
33 6
        $this->ttl = $ttl;
34 6
        if ($namespace !== null) {
35 1
            $this->setNamespace($namespace);
36
        }
37 6
    }
38
39
    /**
40
     * Set the cache namespace.
41
     *
42
     * @param string $namespace the cache namespace.
43
     */
44 1
    public function setNamespace(string $namespace) : bool {
45 1
        $namespace = trim($namespace, '//, ');
46 1
        $dir = $this->getBasePath($namespace);
47
48 1
        if ((is_dir($dir) && is_writable($dir)) || mkdir($dir, 0755)) {
49 1
            $this->namespace = $namespace;
50 1
            return true;
51
        }
52
53
        return false;
54
    }
55
56
    /**
57
     * Set a value to a key on cache.
58
     *
59
     * @param string $key the key to be setted.
60
     * @param mixed $value the correspondent value of that cache key.
61
     * @param int|null $ttl the cache life time in seconds (If no value passed will use the default value).
62
     *
63
     * @return bool true on success or false on failure.
64
     */
65 5
    public function set(string $key, $value, int $ttl = null) : bool {
66
67 5
        $filePath = $this->getFilePath($key);
68
69
        // Uses the ttl passed in the function call or uses the default value
70 5
        $ttl      = $ttl ?? $this->ttl;
71 5
        $interval = new DateInterval(empty($ttl) ? 'P100Y' : "PT{$ttl}S");
72 5
        $date     = new DateTime();
73
74 5
        $fileData = json_encode([
75 5
            'value'      => serialize($value),
76 5
            'created_at' => $date->format('Y-m-d H:i:s'),
77 5
            'expires_at' => $date->add($interval)->format('Y-m-d H:i:s')
78
        ]);
79
80 5
        return file_put_contents($filePath, $fileData);
81
    }
82
83
    /**
84
     * Return the valid cache value stored with the given key.
85
     *
86
     * @param string $key the cache key to be found.
87
     *
88
     * @return mixed the data found.
89
     */
90 4
    public function get(string $key) {
91
92 4
        $fileData = $this->getFileData($key);
93
94 4
        if (empty($fileData) || (date('Y-m-d H:i:s') > $fileData['expires_at'])) {
95 3
            return null;
96
        }
97
98 4
        return unserialize($fileData['value']);
99
    }
100
101
    /**
102
     * Delete cache especified by key.
103
     *
104
     * @param string $key the cache key to be deleted.
105
     *
106
     * @return bool true on success or false on failure.
107
     */
108 1
    public function delete(string $key) : bool {
109 1
        return unlink($this->getFilePath($key));
110
    }
111
112
    /**
113
     * Check if given key exists and is valid on cache.
114
     *
115
     * @param string $key the cache key to be verified.
116
     * @param bool $isValid if set to true the function will verify if it is valid (not expired).
117
     *
118
     * @return bool true if exists false otherwise.
119
     */
120 1
    public function exists(string $key, bool $isValid = false) : bool {
121 1
        $fileData = $this->getFileData($key);
122 1
        return !(empty($fileData) || ($isValid && date('Y-m-d H:i:s') > $fileData['expires_at']));
123
    }
124
125
    /**
126
     * Renew the cache expiration time.
127
     *
128
     * @param string $key the cache key to be renewed.
129
     * @param int|null $ttl extra time to live in seconds.
130
     *
131
     * @return bool true on success or false on failure.
132
     */
133 1
    public function renew(string $key, int $ttl = null) : bool {
134
135 1
        $filePath = $this->getFilePath($key);
136 1
        $fileData = $this->getFileData($key);
137
138 1
        if (empty($fileData)) {
139
            return false;
140
        }
141
142 1
        $ttl = $ttl ?? $this->ttl;
143 1
        $interval = new DateInterval(empty($ttl) ? 'P100Y' : "PT{$ttl}S");
144 1
        $fileData['expires_at'] = (new DateTime())->add($interval)->format('Y-m-d H:i:s');
145 1
        file_put_contents($filePath, json_encode($fileData));
146 1
        return true;
147
    }
148
149
    /**
150
     * Clear all cache files at directory.
151
     *
152
     * @param string|null $namespace the cache namespace.
153
     *
154
     * @return bool true on success or false on failure.
155
     */
156 1
    public function clear(string $namespace = null) : bool {
157
158 1
        $dir = $this->getBasePath($namespace);
159 1
        $files = $this->streamSafeGlob($dir, '*.cache' . self::JSON_FORMAT);
160 1
        foreach ($files as $file) {
161 1
            if (is_file($file)) {
162 1
                if (!unlink($file)) {
163 1
                    return false;
164
                }
165
            }
166
        }
167 1
        return true;
168
    }
169
170
    /**
171
     * Set the directory where cache operations will happen.
172
     *
173
     * @param string $cacheDirectory the directory where cache operations will happen.
174
     *
175
     * @return bool true on success or false on failure.
176
     */
177 6
    public function setCacheDirectory(string $cacheDirectory) : bool {
178 6
        $this->cacheDirectory = null;
179 6
        $cacheDirectory = rtrim($cacheDirectory, '//, ') . '/';
180 6
        if (!((file_exists($cacheDirectory) && is_writable($cacheDirectory)) || mkdir($cacheDirectory, 0755, true))) {
181
            $this->setLastError(new Exception("Failed to use $cacheDirectory as cache directory."));
182
            return false;
183
        }
184 6
        $this->cacheDirectory = $cacheDirectory;
185 6
        return true;
186
    }
187
188
    /**
189
     * Return the last error occurred.
190
     *
191
     * @return array the array with the last error data.
192
     */
193
    public function getLastError() : array {
194
        return $this->lastError;
195
    }
196
197
    /**
198
     * Return the path where the cache file should be located.
199
     *
200
     * @param string $key the cache key
201
     *
202
     * @return string the file path
203
     */
204 5
    private function getFilePath(string $key) : string {
205 5
        return $this->getBasePath($this->namespace) . $key . ".cache" . self::JSON_FORMAT;
206
    }
207
208
    /**
209
     * Return the base path where the cache files should be located.
210
     *
211
     * @return string the file path
212
     */
213 6
    private function getBasePath(string $namespace = null) : string {
214 6
        return $this->cacheDirectory . (!empty($namespace) ? $namespace . DIRECTORY_SEPARATOR : '');
215
    }
216
217
    /**
218
     * Set the last error that ocurred using the lib
219
     *
220
     * @param Exception $ex the exception
221
     */
222
    private function setLastError(Exception $ex) {
223
        $this->lastError['code']    = $ex->getCode();
224
        $this->lastError['message'] = $ex->getMessage();
225
        $this->lastError['trace']   = $ex->getTraceAsString();
226
    }
227
228
    /**
229
     * Get the file data
230
     *
231
     * @param string $key the cache key
232
     *
233
     * @return array with the file data or empty array when no data found
234
     */
235 5
    private function getFileData(string $key) : array {
236 5
        $filePath = $this->getFilePath($key);
237 5
        $contents = @file_get_contents($filePath);
238
239 5
        if (!$contents) {
240 2
            return [];
241 5
        } else if (($data = json_decode($contents, true)) === null && json_last_error() != JSON_ERROR_NONE) {
242
            $this->setLastError(new CacheException(sprintf("Failed to decode the %s data.", $key)));
243
            return [];
244
        }
245
246 5
        return $data;
247
    }
248
249
    /**
250
     * Glob that is safe with streams (vfs for example)
251
     *
252
     * @param string $directory the directory
253
     * @param string $filePattern the file pattern
254
     *
255
     * @return array containing match files
256
     */
257 1
    private function streamSafeGlob($directory, $filePattern) : array {
258 1
        $files = scandir($directory);
259 1
        $found = [];
260
261 1
        foreach ($files as $filename) {
262 1
            if (in_array($filename, ['.', '..'])) {
263 1
                continue;
264
            }
265
266 1
            if (fnmatch($filePattern, $filename)) {
267 1
                $found[] = "{$directory}/{$filename}";
268
            }
269
        }
270
271 1
        return $found;
272
    }
273
}