Passed
Pull Request — master (#332)
by
unknown
08:50
created

FileStore   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 383
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 103
dl 0
loc 383
ccs 118
cts 118
cp 1
rs 8.96
c 2
b 0
f 0
wmc 43

18 Methods

Rating   Name   Duplication   Size   Complexity  
A setCacheDir() 0 5 1
A delete() 0 16 3
A createCacheDir() 0 4 2
A get() 0 19 4
A getCacheFile() 0 3 1
A createCacheFile() 0 8 2
A set() 0 17 4
A getCacheContents() 0 9 2
A __construct() 0 7 1
A getActualCacheKey() 0 9 2
A isValid() 0 12 3
A getCacheDir() 0 3 1
A setCacheFile() 0 5 1
A keys() 0 9 2
A put() 0 6 1
A acquireLock() 0 11 3
B lockedGet() 0 31 7
A lockedSet() 0 17 3

How to fix   Complexity   

Complex Class

Complex classes like FileStore 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.

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 FileStore, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace TusPhp\Cache;
4
5
use Carbon\Carbon;
6
use TusPhp\Config;
7
8
class FileStore extends AbstractCache
9
{
10
    /** @var string */
11
    protected $cacheDir;
12
13
    /** @var string */
14
    protected $cacheFile;
15
16
    /**
17
     * FileStore constructor.
18
     *
19
     * @param string|null $cacheDir
20
     * @param string|null $cacheFile
21
     */
22 3
    public function __construct(string $cacheDir = null, string $cacheFile = null)
23
    {
24 3
        $cacheDir  = $cacheDir ?? Config::get('file.dir');
25 3
        $cacheFile = $cacheFile ?? Config::get('file.name');
26
27 3
        $this->setCacheDir($cacheDir);
28 3
        $this->setCacheFile($cacheFile);
29 3
    }
30
31
    /**
32
     * Set cache dir.
33
     *
34
     * @param string $path
35
     *
36
     * @return self
37
     */
38 2
    public function setCacheDir(string $path): self
39
    {
40 2
        $this->cacheDir = $path;
41
42 2
        return $this;
43
    }
44
45
    /**
46
     * Get cache dir.
47
     *
48
     * @return string
49
     */
50 1
    public function getCacheDir(): string
51
    {
52 1
        return $this->cacheDir;
53
    }
54
55
    /**
56
     * Set cache file.
57
     *
58
     * @param string $file
59
     *
60
     * @return self
61
     */
62 2
    public function setCacheFile(string $file): self
63
    {
64 2
        $this->cacheFile = $file;
65
66 2
        return $this;
67
    }
68
69
    /**
70
     * Get cache file.
71
     *
72
     * @return string
73
     */
74 6
    public function getCacheFile(): string
75
    {
76 6
        return $this->cacheDir . $this->cacheFile;
77
    }
78
79
    /**
80
     * Create cache dir if not exists.
81
     *
82
     * @return void
83
     */
84 3
    protected function createCacheDir()
85
    {
86 3
        if ( ! file_exists($this->cacheDir)) {
87 3
            mkdir($this->cacheDir);
88
        }
89 3
    }
90
91
    /**
92
     * Create a cache file.
93
     *
94
     * @return void
95
     */
96 3
    protected function createCacheFile()
97
    {
98 3
        $this->createCacheDir();
99
100 3
        $cacheFilePath = $this->getCacheFile();
101
102 3
        if ( ! file_exists($cacheFilePath)) {
103 3
            touch($cacheFilePath);
104
        }
105 3
    }
106
107
    /**
108
     * {@inheritDoc}
109
     */
110 9
    public function get(string $key, bool $withExpired = false)
111
    {
112 9
        $key = $this->getActualCacheKey($key);
113
114 9
        return $this->lockedGet(
115 9
            $this->getCacheFile(),
116 9
            false,
117
            function ($handler, array $content) use ($withExpired, $key) {
118 9
                if (empty($content[$key])) {
119 3
                    return null;
120
                }
121
122 6
                $meta = $content[$key];
123
124 6
                if ( ! $withExpired && ! $this->isValid($meta)) {
125 2
                    return null;
126
                }
127
128 5
                return $meta;
129 9
            }
130
        );
131
    }
132
133
    /**
134
     * Write the contents of a file with exclusive lock.
135
     *
136
     * It is not recommended to use this method, for updating files as it will nullify any changes that have been made
137
     * to the file between retrieving $contents and writing the changes. As such, one should instead use lockedSet.
138
     *
139
     * @param string $path
140
     * @param string $contents
141
     *
142
     * @return int|bool The amount of bytes that were written, or false if the write failed
143
     * @see LockedFileStore::lockedSet
144
     *
145
     * @deprecated It is not recommended to use this method, use `lockedSet` instead.
146
     */
147 1
    public function put(string $path, string $contents)
148
    {
149 1
        return $this->lockedSet(
150 1
            $path,
151
            function () use ($contents) {
152 1
                return $contents;
153 1
            }
154
        );
155
    }
156
157
    /**
158
     * {@inheritDoc}
159
     *
160
     * @return int|bool The amount of bytes that were written, or false if the write failed
161
     */
162 8
    public function set(string $key, $value)
163
    {
164 8
        $cacheKey  = $this->getActualCacheKey($key);
165 8
        $cacheFile = $this->getCacheFile();
166
167 8
        if ( ! file_exists($cacheFile)) {
168 8
            $this->createCacheFile();
169
        }
170
171
        return $this->lockedSet($cacheFile, function (array $data) use ($value, $cacheKey) {
172 8
            if ( ! empty($data[$cacheKey]) && \is_array($value)) {
173 3
                $data[$cacheKey] = $value + $data[$cacheKey];
174
            } else {
175 8
                $data[$cacheKey] = $value;
176
            }
177
178 8
            return $data;
179 8
        });
180
    }
181
182
    /**
183
     * {@inheritDoc}
184
     */
185 1
    public function delete(string $key): bool
186
    {
187 1
        $cacheKey = $this->getActualCacheKey($key);
188 1
        $deletion = false;
189
190 1
        return $this->lockedSet(
191 1
            $this->getCacheFile(),
192
            function ($data) use ($cacheKey, &$deletion) {
193 1
                if (isset($data[$cacheKey])) {
194 1
                    unset($data[$cacheKey]);
195 1
                    $deletion = true;
196
                }
197
198 1
                return $data;
199 1
            }
200 1
        ) !== false && $deletion;
201
    }
202
203
    /**
204
     * {@inheritDoc}
205
     */
206 2
    public function keys() : array
207
    {
208 2
        $contents = $this->getCacheContents();
209
210 2
        if (\is_array($contents)) {
211 1
            return array_keys($contents);
212
        }
213
214 1
        return [];
215
    }
216
217
    /**
218
     * Check if cache is still valid.
219
     *
220
     * @param string|array $meta The cache key, or the metadata object
221
     * @return bool
222
     */
223 4
    public function isValid($meta): bool
224
    {
225 4
        if ( ! \is_array($meta)) {
226 2
            $key  = $this->getActualCacheKey($meta);
227 2
            $meta = $this->lockedGet($this->getCacheFile())[$key] ?? [];
228
        }
229
230 4
        if (empty($meta['expires_at'])) {
231 1
            return false;
232
        }
233
234 3
        return Carbon::now() < Carbon::createFromFormat(self::RFC_7231, $meta['expires_at']);
235
    }
236
237
    /**
238
     * Get cache contents.
239
     *
240
     * @return array|bool
241
     */
242 3
    public function getCacheContents()
243
    {
244 3
        $cacheFile = $this->getCacheFile();
245
246 3
        if ( ! file_exists($cacheFile)) {
247 1
            return false;
248
        }
249
250 2
        return $this->lockedGet($cacheFile);
251
    }
252
253
    /**
254
     * Get actual cache key with prefix.
255
     *
256
     * @param string $key
257
     *
258
     * @return string
259
     */
260 1
    public function getActualCacheKey(string $key): string
261
    {
262 1
        $prefix = $this->getPrefix();
263
264 1
        if (false === strpos($key, $prefix)) {
265 1
            $key = $prefix . $key;
266
        }
267
268 1
        return $key;
269
    }
270
271
    //region File lock related operations
272
273
    /**
274
     * Acquire a lock on the given handle
275
     *
276
     * @param resource $handle
277
     * @param int $lock The lock operation (`LOCK_SH`, `LOCK_EX`, or `LOCK_UN`)
278
     * @param int $attempts The number of attempts before returning false
279
     * @return bool True if the lock operation was successful
280
     *
281
     * @see LOCK_SH
282
     * @see LOCK_EX
283
     * @see LOCK_UN
284
     */
285 5
    public function acquireLock($handle, int $lock, int $attempts = 5): bool
286
    {
287 5
        for ($i = 0; $i < $attempts; $i++) {
288 5
            if (flock($handle, $lock)) {
289 4
                return true;
290
            }
291
292 1
            usleep(100);
293
        }
294
295 1
        return false;
296
    }
297
298
    /**
299
     * Get contents of a file with shared access.
300
     *
301
     * Example of the callable:
302
     * ```php
303
     * $property = function($handler, array $data) {
304
     *      return $data['someProperty'];
305
     * }
306
     *
307
     * // $property now has the contents of $data['someProperty']
308
     * ```
309
     *
310
     * @param string $path
311
     * @param bool $exclusive Whether we should use an exclusive or shared lock. If you are writing to the file, an
312
     *  exclusive lock is recommended
313
     * @param callable|null $callback The callable to call when we have locked the file. The first argument is the
314
     *  handle, the second is an array with the cache contents. If left empty, a default callable will be used that
315
     *  returns the data of the file. The callable will not be called if reading the cache fails.
316
     * @return mixed The output of the callable
317
     */
318 5
    public function lockedGet(string $path, bool $exclusive = false, ?callable $callback = null)
319
    {
320 5
        if ($callback === null) {
321
            $callback = function ($handler, $data) {
322 2
                return $data;
323 3
            };
324
        }
325
326 5
        if ( ! file_exists($path)) {
327 2
            $this->createCacheFile();
328
        }
329
330 5
        $handle = @fopen($path, 'r+b');
331 5
        $lock   = $exclusive ? LOCK_EX : LOCK_SH;
332
333 5
        if ($handle === false || ! $this->acquireLock($handle, $lock)) {
334 1
            return null;
335
        }
336
337
        try {
338 4
            clearstatcache(true, $path);
339
340 4
            $contents = fread($handle, filesize($path) ?: 1);
341
342
            // Read the JSON data
343 4
            $data = @json_decode($contents, true) ?? [];
344
345 4
            return $callback($handle, $data);
346
        } finally {
347 4
            $this->acquireLock($handle, LOCK_UN);
348 4
            fclose($handle);
349
        }
350
    }
351
352
    /**
353
     * Write contents to the given path while locking the file
354
     *
355
     * This locks the file during the callback: no other instances can read/modify the file while this operation is
356
     * taking place. As such, one should use the data provided as an argument in the callback for modifying the file
357
     * to prevent the loss of data.
358
     *
359
     * Example of the callable:
360
     * ```php
361
     * function(array $data) {
362
     *     $data['someProperty'] = true;
363
     *
364
     *     return $data;
365
     * }
366
     * ```
367
     *
368
     * @param string $path The path of the file to write to
369
     * @param callable $callback A callable for transforming the data. The first argument of the callable is the current
370
     *  file contents, the return value will be the new contents (which will be json encoded if it is not a string
371
     *  already)
372
     * @return int|bool The amount of bytes that were written, or false if the write failed
373
     */
374 2
    public function lockedSet(string $path, callable $callback)
375
    {
376
        $output = $this->lockedGet($path, true, function ($handle, array $data) use ($callback) {
377 2
            $data = $callback($data) ?? [];
378
379 2
            ftruncate($handle, 0);
380 2
            rewind($handle);
381
382 2
            $data = \is_string($data) ? $data : json_encode($data);
383 2
            $write = fwrite($handle, $data);
384
385 2
            fflush($handle);
386
387 2
            return $write;
388 2
        });
389
390 2
        return $output === null ? false : $output;
391
    }
392
    //endregion
393
}
394