Completed
Push — master ( 6decf3...3556c1 )
by Rasmus
01:52
created

FileCache   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 268
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
wmc 41
lcom 1
cbo 0
dl 0
loc 268
rs 8.2769
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 3
B get() 0 34 6
C set() 0 34 8
A delete() 0 4 1
A clear() 0 8 2
A getMultiple() 0 10 2
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 Psr\SimpleCache\CounterInterface;
11
use RecursiveDirectoryIterator;
12
use RecursiveIteratorIterator;
13
14
/**
15
 * This is a simple, file-based cache implementation, which is bootstrapped by
16
 * the Core Provider as a default.
17
 *
18
 * Bootstrapping a more powerful cache for production scenarios is highly recommended.
19
 *
20
 * @link https://github.com/matthiasmullie/scrapbook/
21
 */
22
class FileCache implements CacheInterface, CounterInterface
23
{
24
    // TODO garbage collection
25
26
    /**
27
     * @var string control characters for keys, reserved by PSR-16
28
     */
29
    const PSR16_RESERVED = '/\{|\}|\(|\)|\/|\\\\|\@|\:/u';
30
31
    /**
32
     * @var string
33
     */
34
    private $cache_path;
35
36
    /**
37
     * @var int
38
     */
39
    private $default_ttl;
40
41
    /**
42
     * @param string $cache_path  absolute root path of cache-file folder
43
     * @param int    $default_ttl default time-to-live (in seconds)
44
     *
45
     * @throws InvalidArgumentException if the specified cache-path does not exist (or is not writable)
46
     */
47
    public function __construct($cache_path, $default_ttl)
48
    {
49
        $path = realpath($cache_path);
50
51
        if ($path === false) {
52
            throw new InvalidArgumentException("cache path does not exist: {$cache_path}");
53
        }
54
55
        if (! is_writable($path . DIRECTORY_SEPARATOR)) {
56
            throw new InvalidArgumentException("cache path is not writable: {$cache_path}");
57
        }
58
59
        $this->cache_path = $path;
60
        $this->default_ttl = $default_ttl;
61
    }
62
63
    public function get($key, $default = null)
64
    {
65
        $path = $this->getPath($key);
66
67
        $expires_at = @filemtime($path);
68
69
        if ($expires_at === false) {
70
            return $default; // file not found
71
        }
72
73
        if ($this->getTime() >= $expires_at) {
74
            @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...
75
76
            return $default;
77
        }
78
79
        $data = @file_get_contents($path);
80
81
        if ($data === false) {
82
            return $default; // race condition: file not found
83
        }
84
85
        if ($data === 'b:0;') {
86
            return false; // because we can't otherwise distinguish a FALSE return-value from unserialize()
87
        }
88
89
        $value = @unserialize($data);
90
91
        if ($value === false) {
92
            return $default; // unserialize() failed
93
        }
94
95
        return $value;
96
    }
97
98
    public function set($key, $value, $ttl = null)
99
    {
100
        $path = $this->getPath($key);
101
102
        $dir = dirname($path);
103
104
        if (! file_exists($dir)) {
105
            @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...
106
        }
107
108
        $temp_path = $this->cache_path . DIRECTORY_SEPARATOR . uniqid('', true);
109
110
        if (is_int($ttl)) {
111
            $expires_at = $this->getTime() + $ttl;
112
        } elseif ($ttl instanceof DateInterval) {
113
            $expires_at = date_create_from_format("U", $this->getTime())->add($ttl)->getTimestamp();
114
        } elseif ($ttl === null) {
115
            $expires_at = $this->getTime() + $this->default_ttl;
116
        } else {
117
            throw new InvalidArgumentException("invalid TTL: " . print_r($ttl, true));
118
        }
119
120
        if (false === @file_put_contents($temp_path, serialize($value))) {
121
            return false;
122
        }
123
124
        if (@touch($temp_path, $expires_at) && @rename($temp_path, $path)) {
125
            return true;
126
        }
127
128
        @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...
129
130
        return false;
131
    }
132
133
    public function delete($key)
134
    {
135
        @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...
136
    }
137
138
    public function clear()
139
    {
140
        $paths = $this->listPaths();
141
142
        foreach ($paths as $path) {
143
            @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...
144
        }
145
    }
146
147
    public function getMultiple($keys)
148
    {
149
        $values = [];
150
151
        foreach ($keys as $key) {
152
            $values[$key] = $this->get($key);
153
        }
154
155
        return $values;
156
    }
157
158
    public function setMultiple($items, $ttl = null)
159
    {
160
        $ok = true;
161
162
        foreach ($items as $key => $value) {
163
            $ok = $this->set($key, $value, $ttl) && $ok;
164
        }
165
166
        return $ok;
167
    }
168
169
    public function deleteMultiple($keys)
170
    {
171
        foreach ($keys as $key) {
172
            $this->delete($key);
173
        }
174
    }
175
176
    public function has($key)
177
    {
178
        return $this->get($key, $this) !== $this;
179
    }
180
181
    public function increment($key, $step = 1)
182
    {
183
        $path = $this->getPath($key);
184
185
        $dir = dirname($path);
186
187
        if (! file_exists($dir)) {
188
            @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...
189
        }
190
191
        $lock_path = $dir . DIRECTORY_SEPARATOR . ".lock"; // allows max. 256 client locks at one time
192
193
        $lock_handle = fopen($lock_path, "w");
194
195
        flock($lock_handle, LOCK_EX);
196
197
        $value = $this->get($key, 0) + $step;
198
199
        $ok = $this->set($key, $value);
200
201
        flock($lock_handle, LOCK_UN);
202
203
        return $ok ? $value : false;
204
    }
205
206
    public function decrement($key, $step = 1)
207
    {
208
        return $this->increment($key, -$step);
209
    }
210
211
    /**
212
     * Clean up expired cache-files.
213
     *
214
     * This method is outside the scope of the PSR-16 cache concept, and is specific to
215
     * this implementation, being a file-cache.
216
     *
217
     * In scenarios with dynamic keys (such as Session IDs) you should call this method
218
     * periodically - for example from a scheduled daily cron-job.
219
     *
220
     * @return void
221
     */
222
    public function cleanExpired()
223
    {
224
        $now = $this->getTime();
225
226
        $paths = $this->listPaths();
227
228
        foreach ($paths as $path) {
229
            if ($now > filemtime($path)) {
230
                @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...
231
            }
232
        }
233
    }
234
235
    /**
236
     * For a given cache key, obtain the absolute file path
237
     *
238
     * @param string $key
239
     *
240
     * @return string absolute path to cache-file
241
     *
242
     * @throws InvalidArgumentException if the specified key contains a character reserved by PSR-16
243
     */
244
    protected function getPath($key)
245
    {
246
        if (preg_match(self::PSR16_RESERVED, $key, $match) === 1) {
247
            throw new InvalidArgumentException("invalid character in key: {$match[0]}");
248
        }
249
250
        $hash = hash("sha256", $key);
251
252
        return $this->cache_path
253
            . DIRECTORY_SEPARATOR
254
            . strtoupper($hash[0])
255
            . DIRECTORY_SEPARATOR
256
            . strtoupper($hash[1])
257
            . DIRECTORY_SEPARATOR
258
            . substr($hash, 2);
259
    }
260
261
    /**
262
     * @return int current timestamp
263
     */
264
    protected function getTime()
265
    {
266
        return time();
267
    }
268
269
    /**
270
     * @return Generator|string[]
271
     */
272
    protected function listPaths()
273
    {
274
        $iterator = new RecursiveDirectoryIterator(
275
            $this->cache_path,
276
            FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS
277
        );
278
279
        $iterator = new RecursiveIteratorIterator($iterator);
280
281
        foreach ($iterator as $path) {
282
            if (is_dir($path)) {
283
                continue; // ignore directories
284
            }
285
286
            yield $path;
287
        }
288
    }
289
}
290