Completed
Pull Request — master (#8)
by
unknown
18:26
created

Directory::loadExpire()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 2
Bugs 0 Features 2
Metric Value
c 2
b 0
f 2
dl 0
loc 16
ccs 5
cts 5
cp 1
rs 9.4286
cc 3
eloc 8
nc 3
nop 1
crap 3
1
<?php
2
3
namespace Apix\Cache;
4
5
/**
6
 * Class Directory
7
 * Directory cache wrapper.
8
 * Expiration time and tags are stored separately from the cached data
9
 *
10
 * @package Apix\Cache
11
 * @author  MacFJA
12
 */
13
class Directory extends AbstractCache
14
{
15
    /**
16
     * Constructor.
17
     *
18
     * @param array  $options Array of options.
19
     */
20 289
    public function __construct(array $options=null)
21
    {
22
        $options += array(
23 289
            'directory' => sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'apix-cache',
24 289
            'locking' => true
25 289
        );
26 289
        parent::__construct(null, $options);
27 289
        $this->initDirectories();
28
    }
29
30
    /**
31
     * Initialize cache directories (create them)
32 289
     */
33
    protected function initDirectories()
34 289
    {
35 289
        $this->getBasePath('key');
36 289
        $this->getBasePath('tag');
37 289
        $this->getBasePath('ttl');
38
    }
39
40
    /**
41
     * Get the base path, and ensure they are created
42
     *
43
     * @param string $type The path type (key, ttl, tag)
44
     * @return string
45 289
     */
46
    protected function getBasePath($type)
47 289
    {
48
        $path = rtrim($this->getOption('directory'), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
49
50 289
        switch ($type) {
51 289
            case 'ttl':
52 289
                $path .= 'ttl' . DIRECTORY_SEPARATOR;
53 289
                break;
54 289
            case 'tags':
55 289
            case 'tag':
56 289
                $path .= 'tag' . DIRECTORY_SEPARATOR;
57
                break;
58 289
        }
59
        $this->buildPath($path);
60 289
61
        return $path;
62
    }
63
64
    /**
65
     * Get the file data.
66
     * If enable, lock file to preserve atomicity
67
     *
68
     * @param string $path The file path
69 272
     * @return string
70
     */
71 272
    protected function readFile($path)
72 272
    {
73 272
        $handle = fopen($path, 'rb');
74 272
        if ($this->getOption('locking')) {
75
            flock($handle, LOCK_SH);
76 272
        }
77
        $data = stream_get_contents($handle);
78
        if ($this->getOption('locking')) {
79
            flock($handle, LOCK_UN);
80
        }
81
        fclose($handle);
82
83
        return $data;
84
    }
85 238
86
    /**
87 238
     * Get the path of a cached data
88 238
     *
89
     * @param string $key The cache key
90 238
     * @return string
91
     */
92
    protected function getKeyPath($key)
93
    {
94
        $dir = $this->getBasePath('key');
95
        $baseKey = base64_encode($key);
96
        $sep = DIRECTORY_SEPARATOR;
97
        $path = $dir . preg_replace('/^(.)(.)(.).+$/', '$1' . $sep . '$2' . $sep . '$3' . $sep . '$0', $baseKey);
98
99 51
        return $path;
100
    }
101 51
102
    /**
103 51
     * Get the path of the expiration file for a key
104 17
     *
105
     * @param string $key The cache key
106
     * @return string
107 51
     */
108
    protected function getTtlPath($key)
109 51
    {
110 17
        $baseKey = base64_encode($key);
111
        $path = $this->getBasePath('ttl') . substr($baseKey, 0, 4);
112
113 34
        return $path;
114
    }
115
116
    /**
117
     * Get the expiration data of a key
118
     *
119
     * @param string $key The cache key
120
     * @return bool|int
121
     */
122 136
    protected function loadExpire($key)
123
    {
124 136
        $path = $this->getTtlPath($key);
125 136
126
        if (!is_file($path)) {
127 136
            return false;
128
        }
129
130
        $expires = json_decode($this->readFile($path), true);
131
132
        if (!array_key_exists(base64_encode($key), $expires)) {
133
            return false;
134
        }
135
136 289
        return $expires[base64_encode($key)];
137
    }
138 289
139 289
    /**
140 289
     * Get the path of a tag file
141 289
     *
142
     * @param string $tag The tag name
143
     * @return string
144
     */
145
    protected function getTagPath($tag)
146
    {
147
        $baseTag = base64_encode($tag);
148
        $path = $this->getBasePath('tag') . $baseTag;
149
150 119
        return $path;
151
    }
152 119
153 119
    /**
154
     * Build and return the path of a directory
155 119
     *
156 119
     * @param string $path The directory path to build
157 119
     * @return mixed
158 119
     */
159
    protected function buildPath($path)
160
    {
161
        if (!is_dir($path)) {
162
            mkdir($path, 0755, true);
163
        }
164
        return $path;
165
    }
166 238
167
    /**
168 238
     * Save a tag
169
     *
170 238
     * @param string $name The tag name
171 238
     * @param string[] $ids The list of cache keys associated to the tag
172
     */
173 238
    protected function saveTag($name, $ids)
174 238
    {
175 17
        $ids = array_unique($ids);
176 17
        array_walk($ids, function(&$item) { $item = base64_encode($item); });
177
178 238
        $path = $this->getTagPath($this->mapTag($name));
179 221
        $this->buildPath(dirname($path));
180 17
        file_put_contents($path, implode(PHP_EOL, $ids), $this->getOption('locking') ? LOCK_EX : null);
181 17
    }
182 204
183
    /**
184 17
     * Save the expiration time of a cache
185 51
     *
186
     * @param string $key the cache key
187
     * @param false|int $ttl The TTL of the cache
188 51
     */
189 51
    protected function saveExpire($key, $ttl)
190
    {
191
        $baseKey = base64_encode($key);
192
193
        $path = $this->getTtlPath($key);
194
        $this->buildPath(dirname($path));
195
196 68
        $expires = array();
197
        if (file_exists($path) && is_file($path)) {
198 68
            $expires = json_decode($this->readFile($path), true);
199 68
        }
200
201 68
        if ($ttl === false) {
202
            if (array_key_exists($baseKey, $expires)) {
203 68
                unset($expires[$baseKey]);
204 68
            } else {
205 68
                return;
206
            }
207
        } else {
208 51
            $expires[$baseKey] = time() + $ttl;
209 68
        }
210
211 68
        file_put_contents($path, json_encode($expires), $this->getOption('locking') ? LOCK_EX : null);
212
    }
213
214
    /**
215
     * Return the list of all existing tags
216
     *
217
     * @return string[]
218
     */
219
    protected function getAllTags()
220 136
    {
221
        $basePath = $this->getBasePath('tag');
222 136
        $baseTags = scandir($basePath);
223
224 136
        $tags = array();
225 136
226 85
        foreach ($baseTags as $baseTag) {
227
            if (substr($baseTag, 0, 1) === '.') {
228
                continue;
229 85
            }
230
231
            $tags[] = $this->removePrefixTag(base64_decode($baseTag));
232
        }
233
234
        return $tags;
235
    }
236
237
    /**
238 136
     * Retrieves the cache content for the given key.
239
     *
240 136
     * @param  string $key The cache key to retrieve.
241 34
     * @return mixed|null Returns the cached data or null.
242
     */
243
    public function loadKey($key)
244 102
    {
245
        $key = $this->mapKey($key);
246 102
247 102
        $path = $this->getKeyPath($key);
248 102
        if (!file_exists($path) && !is_file($path)) {
249
            return null;
250
        }
251 85
252
        return unserialize($this->readFile($path));
253 85
    }
254 17
255
    /**
256
     * Retrieves the cache keys for the given tag.
257 85
     *
258
     * @param  string $tag The cache tag to retrieve.
259 85
     * @return array|null Returns an array of cache keys or null.
260
     */
261
    public function loadTag($tag)
262
    {
263
        if (!$this->getOption('tag_enable')) {
264
            return null;
265
        }
266
267
        $tag = $this->mapTag($tag);
268
269
        $path = $this->getTagPath($tag);
270
        if (!is_file($path)) {
271
            return null;
272 238
        }
273
274 238
        $keys = file($path, FILE_IGNORE_NEW_LINES);
275
276 238
        if (0 === count($keys)) {
277 238
            return null;
278 238
        }
279
280 238
        array_walk($keys, function (&$item) { $item = base64_decode($item); });
281 119
282 119
        return $keys;
283 119
    }
284 119
285 119
    /**
286 119
     * Saves data to the cache.
287
     *
288 238
     * @param  mixed $data The data to cache.
289 51
     * @param  string $key The cache id to save.
290 51
     * @param  array $tags The cache tags for this cache entry.
291 221
     * @param  int $ttl The time-to-live in seconds, if set to null the
292
     *                       cache is valid forever.
293
     * @return boolean Returns True on success or False on failure.
294 238
     */
295
    public function save($data, $key, array $tags = null, $ttl = null)
296
    {
297
        $key = $this->mapKey($key);
298
299
        $path = $this->getKeyPath($key);
300
        $this->buildPath(dirname($path));
301
        file_put_contents($path, serialize($data), $this->getOption('locking') ? LOCK_EX : null);
302
303 85
        if (null !== $tags) {
304
            foreach ($tags as $tag) {
305 85
                $ids = $this->loadTag($tag);
306
                $ids[] = $key;
307 85
                $this->saveTag($tag, $ids);
308 85
            }
309 34
        }
310
311
        if (null !== $ttl) {
312 68
            $this->saveExpire($key, $ttl);
313
        } else {
314 68
            $this->saveExpire($key, false);
315 51
        }
316 51
317 17
        return true;
318
    }
319 34
320 34
    /**
321 34
     * Deletes the specified cache record.
322 34
     *
323 68
     * @param  string $key The cache id to remove.
324
     * @return boolean Returns True on success or False on failure.
325 68
     */
326
    public function delete($key)
327 68
    {
328
        $key = $this->mapKey($key);
329
330
        $path = $this->getKeyPath($key);
331
        if (!is_file($path)) {
332
            return false;
333
        }
334
335
        unlink($path);
336 17
337
        foreach ($this->getAllTags() as $tag) {
338 17
            $ids = $this->loadTag($tag);
339 17
            if (null === $ids) {
340
                continue;
341 17
            }
342 17
            if (in_array($key, $ids, true) !== false) {
343
                unset($ids[array_search($key, $ids, true)]);
344 17
                $this->saveTag($tag, $ids);
345 17
            }
346 17
        }
347 17
348 17
        $this->saveExpire($key, false);
349
350 17
        return true;
351
    }
352
353
    /**
354
     * Removes all the cached entries associated with the given tag names.
355
     *
356
     * @param  array $tags The array of tags to remove.
357
     * @return boolean Returns True on success or False on failure.
358
     */
359
    public function clean(array $tags)
360 289
    {
361
        foreach ($tags as $tag) {
362 289
            $ids = $this->loadTag($tag);
363 289
364 289
            if (null === $ids) {
365
                return false;
366
            }
367
            foreach ($ids as $key) {
368
                $this->delete($this->removePrefixKey($key));
369
            }
370
            unlink($this->getTagPath($this->mapTag($tag)));
371
        }
372 289
373 289
        return true;
374 289
    }
375 289
376 289
    /**
377 289
     * Flush all the cached entries.
378 289
     *
379
     * @param  boolean $all Wether to flush the whole database, or (preferably)
380
     *                      the entries prefixed with prefix_key and prefix_tag.
381
     * @return boolean Returns True on success or False on failure.
382
     */
383
    public function flush($all = false)
384
    {
385
        $this->delTree($this->getOption('directory'));
386
        $this->initDirectories();
387
    }
388 51
389
    /**
390 51
     * Remove a directory
391
     *
392 51
     * @param string $dir The path of the directory to remove
393
     * @return bool
394 51
     */
395 34
    public function delTree($dir) {
396
        $files = array_diff(scandir($dir), array('.','..'));
397
        foreach ($files as $file) {
398 51
            $newPath = $dir . DIRECTORY_SEPARATOR . $file;
399
            (is_dir($newPath)) ? $this->delTree($newPath) : unlink($newPath);
400 51
        }
401 34
        return rmdir($dir);
402
    }
403 34
404
    /**
405
     * Returns the time-to-live (in seconds) for the given key.
406
     *
407
     * @param  string $key The name of the key.
408
     * @return int|false Returns the number of seconds left, 0 if valid
409
     *                       forever or False if the key is non-existant.
410
     */
411
    public function getTtl($key)
412
    {
413
        $key = $this->mapKey($key);
414
415
        $path = $this->getKeyPath($key);
416
417
        if (!file_exists($path) && !is_file($path)) {
418
            return false;
419
        }
420
421
        $expire = $this->loadExpire($key);
422
423
        if (false === $expire) {
424
            return 0;
425
        }
426
        return $expire - time();
0 ignored issues
show
Bug Compatibility introduced by
The expression $expire - time(); of type integer|double adds the type double to the return on line 426 which is incompatible with the return type declared by the interface Apix\Cache\Adapter::getTtl of type integer|false.
Loading history...
427
    }
428
}