Completed
Pull Request — master (#2)
by
unknown
12:34
created

FileCache   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 272
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 90.43%

Importance

Changes 0
Metric Value
wmc 44
lcom 1
cbo 0
dl 0
loc 272
ccs 104
cts 115
cp 0.9043
rs 8.3396
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 19 5
B get() 0 34 6
C set() 0 34 8
A delete() 0 4 1
A clear() 0 8 2
A getMultiple() 0 10 3
A setMultiple() 0 10 3
A deleteMultiple() 0 6 2
A has() 0 4 1
B increment() 0 24 3
A decrement() 0 4 1
A cleanExpired() 0 12 3
A getPath() 0 16 2
A getTime() 0 4 1
A listPaths() 0 17 3

How to fix   Complexity   

Complex Class

Complex classes like FileCache 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 FileCache, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Kodus\Cache;
4
5
use DateInterval;
6
use FilesystemIterator;
7
use Generator;
8
use InvalidArgumentException;
9
use Psr\SimpleCache\CacheInterface;
10
use RecursiveDirectoryIterator;
11
use RecursiveIteratorIterator;
12
13
/**
14
 * This is a simple, file-based cache implementation, which is bootstrapped by
15
 * the Core Provider as a default.
16
 *
17
 * Bootstrapping a more powerful cache for production scenarios is highly recommended.
18
 *
19
 * @link https://github.com/matthiasmullie/scrapbook/
20
 */
21
class FileCache implements CacheInterface
22
{
23
    // TODO garbage collection
24
25
    /**
26
     * @var string control characters for keys, reserved by PSR-16
27
     */
28
    const PSR16_RESERVED = '/\{|\}|\(|\)|\/|\\\\|\@|\:/u';
29
30
    /**
31
     * @var string
32
     */
33
    private $cache_path;
34
35
    /**
36
     * @var int
37
     */
38
    private $default_ttl;
39
40
    /**
41
     * @param string $cache_path  absolute root path of cache-file folder
42
     * @param int    $default_ttl default time-to-live (in seconds)
43
     *
44
     * @throws InvalidArgumentException if the specified cache-path does not exist (or is not writable)
45
     */
46 13
    public function __construct($cache_path, $default_ttl)
47
    {
48 13
        if (! file_exists($cache_path) && file_exists(dirname($cache_path))) {
49 13
            @mkdir($cache_path, 0777); // ensure that the parent path exists
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
50
        }
51
52 13
        $path = realpath($cache_path);
53
54 13
        if ($path === false) {
55
            throw new InvalidArgumentException("cache path does not exist: {$cache_path}");
56
        }
57
58 13
        if (! is_writable($path . DIRECTORY_SEPARATOR)) {
59
            throw new InvalidArgumentException("cache path is not writable: {$cache_path}");
60
        }
61
62 13
        $this->cache_path = $path;
63 13
        $this->default_ttl = $default_ttl;
64 13
    }
65
66 11
    public function get($key, $default = null)
67
    {
68 11
        $path = $this->getPath($key);
69
70 11
        $expires_at = @filemtime($path);
71
72 11
        if ($expires_at === false) {
73 10
            return $default; // file not found
74
        }
75
76 9
        if ($this->getTime() >= $expires_at) {
77 4
            @unlink($path); // file expired
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
78
79 4
            return $default;
80
        }
81
82 8
        $data = @file_get_contents($path);
83
84 8
        if ($data === false) {
85
            return $default; // race condition: file not found
86
        }
87
88 8
        if ($data === 'b:0;') {
89
            return false; // because we can't otherwise distinguish a FALSE return-value from unserialize()
90
        }
91
92 8
        $value = @unserialize($data);
93
94 8
        if ($value === false) {
95
            return $default; // unserialize() failed
96
        }
97
98 8
        return $value;
99
    }
100
101 12
    public function set($key, $value, $ttl = null)
102
    {
103 12
        $path = $this->getPath($key);
104
105 12
        $dir = dirname($path);
106
107 12
        if (! file_exists($dir)) {
108 10
            @mkdir($dir, 0777, true); // ensure that the parent path exists
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
109
        }
110
111 12
        $temp_path = $this->cache_path . DIRECTORY_SEPARATOR . uniqid('', true);
112
113 12
        if (is_int($ttl)) {
114 3
            $expires_at = $this->getTime() + $ttl;
115
        } elseif ($ttl instanceof DateInterval) {
116 1
            $expires_at = date_create_from_format("U", $this->getTime())->add($ttl)->getTimestamp();
117 8
        } elseif ($ttl === null) {
118 7
            $expires_at = $this->getTime() + $this->default_ttl;
119
        } else {
120 1
            throw new InvalidArgumentException("invalid TTL: " . print_r($ttl, true));
121
        }
122
123 11
        if (false === @file_put_contents($temp_path, serialize($value))) {
124
            return false;
125
        }
126
127 11
        if (@touch($temp_path, $expires_at) && @rename($temp_path, $path)) {
128 11
            return true;
129
        }
130
131
        @unlink($temp_path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
132
133
        return false;
134
    }
135
136 2
    public function delete($key)
137
    {
138 2
        @unlink($this->getPath($key));
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
139 2
    }
140
141 12
    public function clear()
142
    {
143 12
        $paths = $this->listPaths();
144
145 12
        foreach ($paths as $path) {
146 7
            @unlink($path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
147
        }
148 12
    }
149
150 1
    public function getMultiple($keys, $default = null)
151
    {
152 1
        $values = [];
153
154 1
        foreach ($keys as $key) {
155 1
            $values[$key] = $this->get($key) ?: $default;
156
        }
157
158 1
        return $values;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $values; (array) is incompatible with the return type declared by the interface Psr\SimpleCache\CacheInterface::getMultiple of type Psr\SimpleCache\iterable.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
159
    }
160
161 2
    public function setMultiple($items, $ttl = null)
162
    {
163 2
        $ok = true;
164
165 2
        foreach ($items as $key => $value) {
166 2
            $ok = $this->set($key, $value, $ttl) && $ok;
167
        }
168
169 1
        return $ok;
170
    }
171
172 1
    public function deleteMultiple($keys)
173
    {
174 1
        foreach ($keys as $key) {
175 1
            $this->delete($key);
176
        }
177 1
    }
178
179 1
    public function has($key)
180
    {
181 1
        return $this->get($key, $this) !== $this;
182
    }
183
184 2
    public function increment($key, $step = 1)
185
    {
186 2
        $path = $this->getPath($key);
187
188 2
        $dir = dirname($path);
189
190 2
        if (! file_exists($dir)) {
191 2
            @mkdir($dir, 0777, true); // ensure that the parent path exists
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
192
        }
193
194 2
        $lock_path = $dir . DIRECTORY_SEPARATOR . ".lock"; // allows max. 256 client locks at one time
195
196 2
        $lock_handle = fopen($lock_path, "w");
197
198 2
        flock($lock_handle, LOCK_EX);
199
200 2
        $value = $this->get($key, 0) + $step;
201
202 2
        $ok = $this->set($key, $value);
203
204 2
        flock($lock_handle, LOCK_UN);
205
206 2
        return $ok ? $value : false;
207
    }
208
209 1
    public function decrement($key, $step = 1)
210
    {
211 1
        return $this->increment($key, -$step);
212
    }
213
214
    /**
215
     * Clean up expired cache-files.
216
     *
217
     * This method is outside the scope of the PSR-16 cache concept, and is specific to
218
     * this implementation, being a file-cache.
219
     *
220
     * In scenarios with dynamic keys (such as Session IDs) you should call this method
221
     * periodically - for example from a scheduled daily cron-job.
222
     *
223
     * @return void
224
     */
225 1
    public function cleanExpired()
226
    {
227 1
        $now = $this->getTime();
228
229 1
        $paths = $this->listPaths();
230
231 1
        foreach ($paths as $path) {
232 1
            if ($now > filemtime($path)) {
233 1
                @unlink($path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
234
            }
235
        }
236 1
    }
237
238
    /**
239
     * For a given cache key, obtain the absolute file path
240
     *
241
     * @param string $key
242
     *
243
     * @return string absolute path to cache-file
244
     *
245
     * @throws InvalidArgumentException if the specified key contains a character reserved by PSR-16
246
     */
247 13
    protected function getPath($key)
248
    {
249 13
        if (preg_match(self::PSR16_RESERVED, $key, $match) === 1) {
250
            throw new InvalidArgumentException("invalid character in key: {$match[0]}");
251
        }
252
253 13
        $hash = hash("sha256", $key);
254
255 13
        return $this->cache_path
256 13
            . DIRECTORY_SEPARATOR
257 13
            . strtoupper($hash[0])
258 13
            . DIRECTORY_SEPARATOR
259 13
            . strtoupper($hash[1])
260 13
            . DIRECTORY_SEPARATOR
261 13
            . substr($hash, 2);
262
    }
263
264
    /**
265
     * @return int current timestamp
266
     */
267 13
    protected function getTime()
268
    {
269 13
        return time();
270
    }
271
272
    /**
273
     * @return Generator|string[]
274
     */
275 12
    protected function listPaths()
276
    {
277 12
        $iterator = new RecursiveDirectoryIterator(
278 12
            $this->cache_path,
279 12
            FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS
280
        );
281
282 12
        $iterator = new RecursiveIteratorIterator($iterator);
283
284 12
        foreach ($iterator as $path) {
285 7
            if (is_dir($path)) {
286
                continue; // ignore directories
287
            }
288
289 7
            yield $path;
290
        }
291 12
    }
292
}
293