Completed
Push — master ( 832a42...a69dc8 )
by Franck
06:59
created

Directory::readFile()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

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