Completed
Push — master ( e2ef81...59820c )
by Franck
16:11
created

Directory::clean()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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