Completed
Push — master ( a47f55...644d7f )
by
unknown
11s
created

FileCache::getMultiple()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 7
cts 7
cp 1
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 7
nc 4
nop 2
crap 5
1
<?php
2
3
namespace Kodus\Cache;
4
5
use DateInterval;
6
use FilesystemIterator;
7
use Generator;
8
use Psr\SimpleCache\CacheInterface;
9
use RecursiveDirectoryIterator;
10
use RecursiveIteratorIterator;
11
use Traversable;
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 12
    public function get($key, $default = null)
67
    {
68 12
        $path = $this->getPath($key);
69
70 12
        $expires_at = @filemtime($path);
71
72 12
        if ($expires_at === false) {
73 11
            return $default; // file not found
74
        }
75
76 10
        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 9
        $data = @file_get_contents($path);
83
84 9
        if ($data === false) {
85
            return $default; // race condition: file not found
86
        }
87
88 9
        if ($data === 'b:0;') {
89
            return false; // because we can't otherwise distinguish a FALSE return-value from unserialize()
90
        }
91
92 9
        $value = @unserialize($data);
93
94 9
        if ($value === false) {
95
            return $default; // unserialize() failed
96
        }
97
98 9
        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 8
            $expires_at = $this->getTime() + $this->default_ttl;
119
        } else {
120
            throw new InvalidArgumentException("invalid TTL: " . print_r($ttl, true));
121
        }
122
123 12
        if (false === @file_put_contents($temp_path, serialize($value))) {
124
            return false;
125
        }
126
127 12
        if (@touch($temp_path, $expires_at) && @rename($temp_path, $path)) {
128 12
            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
        return @unlink($this->getPath($key));
139
    }
140
141 13
    public function clear()
142
    {
143 13
        $success = true;
144
145 13
        $paths = $this->listPaths();
146
147 13
        foreach ($paths as $path) {
148 8
            if (! unlink($path)) {
149 8
                $success = false;
150
            }
151
        }
152
153 13
        return $success;
154
    }
155
156 2
    public function getMultiple($keys, $default = null)
157
    {
158 2
        if (! is_array($keys) && ! $keys instanceof Traversable) {
159 1
            throw new InvalidArgumentException("keys must be either of type array or Traversable");
160
        }
161
162 2
        $values = [];
163
164 2
        foreach ($keys as $key) {
165 2
            $values[$key] = $this->get($key) ?: $default;
166
        }
167
168 2
        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...
169
    }
170
171 2
    public function setMultiple($values, $ttl = null)
172
    {
173 2
        if (! is_array($values) && ! $values instanceof Traversable) {
174 1
            throw new InvalidArgumentException("keys must be either of type array or Traversable");
175
        }
176
177 2
        $ok = true;
178
179 2
        foreach ($values as $key => $value) {
180 2
            $this->validateKey($key);
181 2
            $ok = $this->set($key, $value, $ttl) && $ok;
182
        }
183
184 2
        return $ok;
185
    }
186
187 1
    public function deleteMultiple($keys)
188
    {
189 1
        if (! is_array($keys) && ! $keys instanceof Traversable) {
190 1
            throw new InvalidArgumentException("keys must be either of type array or Traversable");
191
        }
192
193 1
        foreach ($keys as $key) {
194 1
            $this->validateKey($key);
195 1
            $this->delete($key);
196
        }
197 1
    }
198
199 1
    public function has($key)
200
    {
201 1
        return $this->get($key, $this) !== $this;
202
    }
203
204 2
    public function increment($key, $step = 1)
205
    {
206 2
        $path = $this->getPath($key);
207
208 2
        $dir = dirname($path);
209
210 2
        if (! file_exists($dir)) {
211 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...
212
        }
213
214 2
        $lock_path = $dir . DIRECTORY_SEPARATOR . ".lock"; // allows max. 256 client locks at one time
215
216 2
        $lock_handle = fopen($lock_path, "w");
217
218 2
        flock($lock_handle, LOCK_EX);
219
220 2
        $value = $this->get($key, 0) + $step;
221
222 2
        $ok = $this->set($key, $value);
223
224 2
        flock($lock_handle, LOCK_UN);
225
226 2
        return $ok ? $value : false;
227
    }
228
229 1
    public function decrement($key, $step = 1)
230
    {
231 1
        return $this->increment($key, -$step);
232
    }
233
234
    /**
235
     * Clean up expired cache-files.
236
     *
237
     * This method is outside the scope of the PSR-16 cache concept, and is specific to
238
     * this implementation, being a file-cache.
239
     *
240
     * In scenarios with dynamic keys (such as Session IDs) you should call this method
241
     * periodically - for example from a scheduled daily cron-job.
242
     *
243
     * @return void
244
     */
245 1
    public function cleanExpired()
246
    {
247 1
        $now = $this->getTime();
248
249 1
        $paths = $this->listPaths();
250
251 1
        foreach ($paths as $path) {
252 1
            if ($now > filemtime($path)) {
253 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...
254
            }
255
        }
256 1
    }
257
258
    /**
259
     * For a given cache key, obtain the absolute file path
260
     *
261
     * @param string $key
262
     *
263
     * @return string absolute path to cache-file
264
     *
265
     * @throws InvalidArgumentException if the specified key contains a character reserved by PSR-16
266
     */
267 13
    protected function getPath($key)
268
    {
269 13
        $this->validateKey($key);
270
271 13
        $hash = hash("sha256", $key);
272
273 13
        return $this->cache_path
274 13
            . DIRECTORY_SEPARATOR
275 13
            . strtoupper($hash[0])
276 13
            . DIRECTORY_SEPARATOR
277 13
            . strtoupper($hash[1])
278 13
            . DIRECTORY_SEPARATOR
279 13
            . substr($hash, 2);
280
    }
281
282
    /**
283
     * @return int current timestamp
284
     */
285 13
    protected function getTime()
286
    {
287 13
        return time();
288
    }
289
290
    /**
291
     * @return Generator|string[]
292
     */
293 13
    protected function listPaths()
294
    {
295 13
        $iterator = new RecursiveDirectoryIterator(
296 13
            $this->cache_path,
297 13
            FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS
298
        );
299
300 13
        $iterator = new RecursiveIteratorIterator($iterator);
301
302 13
        foreach ($iterator as $path) {
303 8
            if (is_dir($path)) {
304
                continue; // ignore directories
305
            }
306
307 8
            yield $path;
308
        }
309 13
    }
310
311
    /**
312
     * @param string $key
313
     *
314
     * @throws InvalidArgumentException
315
     */
316 13
    protected function validateKey($key)
317
    {
318 13
        if (preg_match(self::PSR16_RESERVED, $key, $match) === 1) {
319 3
            throw new InvalidArgumentException("invalid character in key: {$match[0]}");
320
        }
321 13
    }
322
}
323