Directory   C
last analyzed

Complexity

Total Complexity 57

Size/Duplication

Total Lines 419
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 57
lcom 1
cbo 1
dl 0
loc 419
ccs 177
cts 177
cp 1
rs 6.433
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A initDirectories() 0 6 1
A getBasePath() 0 17 4
A readFile() 0 14 3
A getKeyPath() 0 9 1
A getTtlPath() 0 7 1
A loadExpire() 0 16 3
A getTagPath() 0 7 1
A buildPath() 0 7 2
A saveTag() 0 9 2
B saveExpire() 0 24 6
A getAllTags() 0 17 3
A loadKey() 0 11 3
B loadTag() 0 23 4
B save() 0 24 5
B delete() 0 26 5
A clean() 0 19 4
A flush() 0 5 1
A delTree() 0 8 3
A getTtl() 0 17 4

How to fix   Complexity   

Complex Class

Complex classes like Directory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Directory, and based on these observations, apply Extract Interface, too.

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