Flysystem   F
last analyzed

Complexity

Total Complexity 73

Size/Duplication

Total Lines 505
Duplicated Lines 25.74 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 73
lcom 1
cbo 1
dl 130
loc 505
rs 2.56
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A get() 0 19 3
A getMulti() 0 16 3
B set() 0 23 6
A setMulti() 9 9 2
A delete() 0 19 3
A deleteMulti() 0 9 2
A add() 0 25 5
A replace() 0 25 5
A cas() 26 26 5
A increment() 8 8 3
A decrement() 8 8 3
A touch() 26 26 5
A flush() 18 18 4
A getCollection() 0 16 1
A doIncrement() 21 21 5
A exists() 0 18 4
A lock() 0 16 3
A unlock() 0 11 2
A normalizeTime() 14 14 3
A wrap() 0 6 1
A read() 0 23 3
A path() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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

1
<?php
2
3
namespace MatthiasMullie\Scrapbook\Adapters;
4
5
use League\Flysystem\FileNotFoundException;
6
use League\Flysystem\FileExistsException;
7
use League\Flysystem\Filesystem;
8
use MatthiasMullie\Scrapbook\Adapters\Collections\Flysystem as Collection;
9
use MatthiasMullie\Scrapbook\KeyValueStore;
10
11
/**
12
 * Flysystem adapter. Data will be written to League\Flysystem\Filesystem.
13
 *
14
 * Flysystem doesn't allow locking files, though. To guarantee interference from
15
 * other processes, we'll create separate lock-files to flag a cache key in use.
16
 *
17
 * @author Matthias Mullie <[email protected]>
18
 * @copyright Copyright (c) 2014, Matthias Mullie. All rights reserved
19
 * @license LICENSE MIT
20
 */
21
class Flysystem implements KeyValueStore
22
{
23
    /**
24
     * @var Filesystem
25
     */
26
    protected $filesystem;
27
28
    /**
29
     * @param Filesystem $filesystem
30
     */
31
    public function __construct(Filesystem $filesystem)
32
    {
33
        $this->filesystem = $filesystem;
34
    }
35
36
    /**
37
     * {@inheritdoc}
38
     */
39
    public function get($key, &$token = null)
40
    {
41
        $token = null;
42
43
        // let expired-but-not-yet-deleted files be deleted first
44
        if (!$this->exists($key)) {
45
            return false;
46
        }
47
48
        $data = $this->read($key);
49
        if ($data === false) {
50
            return false;
51
        }
52
53
        $value = unserialize($data[1]);
54
        $token = $data[1];
55
56
        return $value;
57
    }
58
59
    /**
60
     * {@inheritdoc}
61
     */
62
    public function getMulti(array $keys, array &$tokens = null)
63
    {
64
        $results = array();
65
        $tokens = array();
66
        foreach ($keys as $key) {
67
            $token = null;
68
            $value = $this->get($key, $token);
69
70
            if ($token !== null) {
71
                $results[$key] = $value;
72
                $tokens[$key] = $token;
73
            }
74
        }
75
76
        return $results;
77
    }
78
79
    /**
80
     * {@inheritdoc}
81
     */
82
    public function set($key, $value, $expire = 0)
83
    {
84
        // we don't really need a lock for this operation, but we need to make
85
        // sure it's not locked by another operation, which we could overwrite
86
        if (!$this->lock($key)) {
87
            return false;
88
        }
89
90
        $expire = $this->normalizeTime($expire);
91
        if ($expire !== 0 && $expire < time()) {
92
            $this->unlock($key);
93
94
            // don't waste time storing (and later comparing expiration
95
            // timestamp) data that is already expired; just delete it already
96
            return !$this->exists($key) || $this->delete($key);
97
        }
98
99
        $path = $this->path($key);
100
        $data = $this->wrap($value, $expire);
101
        $success = $this->filesystem->put($path, $data);
102
103
        return $success !== false && $this->unlock($key);
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109 View Code Duplication
    public function setMulti(array $items, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
110
    {
111
        $success = array();
112
        foreach ($items as $key => $value) {
113
            $success[$key] = $this->set($key, $value, $expire);
114
        }
115
116
        return $success;
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122
    public function delete($key)
123
    {
124
        if (!$this->lock($key)) {
125
            return false;
126
        }
127
128
        $path = $this->path($key);
129
130
        try {
131
            $this->filesystem->delete($path);
132
            $this->unlock($key);
133
134
            return true;
135
        } catch (FileNotFoundException $e) {
136
            $this->unlock($key);
137
138
            return false;
139
        }
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    public function deleteMulti(array $keys)
146
    {
147
        $success = array();
148
        foreach ($keys as $key) {
149
            $success[$key] = $this->delete($key);
150
        }
151
152
        return $success;
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158
    public function add($key, $value, $expire = 0)
159
    {
160
        if (!$this->lock($key)) {
161
            return false;
162
        }
163
164
        if ($this->exists($key)) {
165
            $this->unlock($key);
166
167
            return false;
168
        }
169
170
        $path = $this->path($key);
171
        $data = $this->wrap($value, $expire);
172
173
        try {
174
            $success = $this->filesystem->write($path, $data);
175
176
            return $success && $this->unlock($key);
177
        } catch (FileExistsException $e) {
178
            $this->unlock($key);
179
180
            return false;
181
        }
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187
    public function replace($key, $value, $expire = 0)
188
    {
189
        if (!$this->lock($key)) {
190
            return false;
191
        }
192
193
        if (!$this->exists($key)) {
194
            $this->unlock($key);
195
196
            return false;
197
        }
198
199
        $path = $this->path($key);
200
        $data = $this->wrap($value, $expire);
201
202
        try {
203
            $success = $this->filesystem->update($path, $data);
204
205
            return $success && $this->unlock($key);
206
        } catch (FileNotFoundException $e) {
207
            $this->unlock($key);
208
209
            return false;
210
        }
211
    }
212
213
    /**
214
     * {@inheritdoc}
215
     */
216 View Code Duplication
    public function cas($token, $key, $value, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
217
    {
218
        if (!$this->lock($key)) {
219
            return false;
220
        }
221
222
        $current = $this->get($key);
223
        if ($token !== serialize($current)) {
224
            $this->unlock($key);
225
226
            return false;
227
        }
228
229
        $path = $this->path($key);
230
        $data = $this->wrap($value, $expire);
231
232
        try {
233
            $success = $this->filesystem->update($path, $data);
234
235
            return $success && $this->unlock($key);
236
        } catch (FileNotFoundException $e) {
237
            $this->unlock($key);
238
239
            return false;
240
        }
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     */
246 View Code Duplication
    public function increment($key, $offset = 1, $initial = 0, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
247
    {
248
        if ($offset <= 0 || $initial < 0) {
249
            return false;
250
        }
251
252
        return $this->doIncrement($key, $offset, $initial, $expire);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression $this->doIncrement($key,...et, $initial, $expire); of type integer|false|double adds the type double to the return on line 252 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::increment of type integer|boolean.
Loading history...
253
    }
254
255
    /**
256
     * {@inheritdoc}
257
     */
258 View Code Duplication
    public function decrement($key, $offset = 1, $initial = 0, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
259
    {
260
        if ($offset <= 0 || $initial < 0) {
261
            return false;
262
        }
263
264
        return $this->doIncrement($key, -$offset, $initial, $expire);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression $this->doIncrement($key,...et, $initial, $expire); of type integer|false|double adds the type double to the return on line 264 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::decrement of type integer|boolean.
Loading history...
265
    }
266
267
    /**
268
     * {@inheritdoc}
269
     */
270 View Code Duplication
    public function touch($key, $expire)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
271
    {
272
        if (!$this->lock($key)) {
273
            return false;
274
        }
275
276
        $value = $this->get($key);
277
        if ($value === false) {
278
            $this->unlock($key);
279
280
            return false;
281
        }
282
283
        $path = $this->path($key);
284
        $data = $this->wrap($value, $expire);
285
286
        try {
287
            $success = $this->filesystem->update($path, $data);
288
289
            return $success && $this->unlock($key);
290
        } catch (FileNotFoundException $e) {
291
            $this->unlock($key);
292
293
            return false;
294
        }
295
    }
296
297
    /**
298
     * {@inheritdoc}
299
     */
300 View Code Duplication
    public function flush()
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
301
    {
302
        $files = $this->filesystem->listContents();
303
        foreach ($files as $file) {
304
            try {
305
                if ($file['type'] === 'dir') {
306
                    $this->filesystem->deleteDir($file['path']);
307
                } else {
308
                    $this->filesystem->delete($file['path']);
309
                }
310
            } catch (FileNotFoundException $e) {
311
                // don't care if we failed to unlink something, might have
312
                // been deleted by another process in the meantime...
313
            }
314
        }
315
316
        return true;
317
    }
318
319
    /**
320
     * {@inheritdoc}
321
     */
322
    public function getCollection($name)
323
    {
324
        /*
325
         * A better solution could be to simply construct a new object for a
326
         * subfolder, but we can't reliably create a new
327
         * `League\Flysystem\Filesystem` object for a subfolder from the
328
         * `$this->filesystem` object we have. I could `->getAdapter` and fetch
329
         * the path from there, but only if we can assume that the adapter is
330
         * `League\Flysystem\Adapter\Local`, which it may not be.
331
         * But I can just create a new object that changes the path to write at,
332
         * by prefixing it with a subfolder!
333
         */
334
        $this->filesystem->createDir($name);
335
336
        return new Collection($this->filesystem, $name);
337
    }
338
339
    /**
340
     * Shared between increment/decrement: both have mostly the same logic
341
     * (decrement just increments a negative value), but need their validation
342
     * split up (increment won't accept negative values).
343
     *
344
     * @param string $key
345
     * @param int    $offset
346
     * @param int    $initial
347
     * @param int    $expire
348
     *
349
     * @return int|bool
350
     */
351 View Code Duplication
    protected function doIncrement($key, $offset, $initial, $expire)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
352
    {
353
        $current = $this->get($key, $token);
354
        if ($current === false) {
355
            $success = $this->add($key, $initial, $expire);
356
357
            return $success ? $initial : false;
358
        }
359
360
        // NaN, doesn't compute
361
        if (!is_numeric($current)) {
362
            return false;
363
        }
364
365
        $value = $current + $offset;
366
        $value = max(0, $value);
367
368
        $success = $this->cas($token, $key, $value, $expire);
369
370
        return $success ? $value : false;
371
    }
372
373
    /**
374
     * @param string $key
375
     *
376
     * @return bool
377
     */
378
    protected function exists($key)
379
    {
380
        $data = $this->read($key);
381
        if ($data === false) {
382
            return false;
383
        }
384
385
        $expire = $data[0];
386
        if ($expire !== 0 && $expire < time()) {
387
            // expired, don't keep it around
388
            $path = $this->path($key);
389
            $this->filesystem->delete($path);
390
391
            return false;
392
        }
393
394
        return true;
395
    }
396
397
    /**
398
     * Obtain a lock for a given key.
399
     * It'll try to get a lock for a couple of times, but ultimately give up if
400
     * no lock can be obtained in a reasonable time.
401
     *
402
     * @param string $key
403
     *
404
     * @return bool
405
     */
406
    protected function lock($key)
407
    {
408
        $path = $key.'.lock';
409
410
        for ($i = 0; $i < 25; ++$i) {
411
            try {
412
                $this->filesystem->write($path, '');
413
414
                return true;
415
            } catch (FileExistsException $e) {
416
                usleep(200);
417
            }
418
        }
419
420
        return false;
421
    }
422
423
    /**
424
     * Release the lock for a given key.
425
     *
426
     * @param string $key
427
     *
428
     * @return bool
429
     */
430
    protected function unlock($key)
431
    {
432
        $path = $key.'.lock';
433
        try {
434
            $this->filesystem->delete($path);
435
        } catch (FileNotFoundException $e) {
436
            return false;
437
        }
438
439
        return true;
440
    }
441
442
    /**
443
     * Times can be:
444
     * * relative (in seconds) to current time, within 30 days
445
     * * absolute unix timestamp
446
     * * 0, for infinity.
447
     *
448
     * The first case (relative time) will be normalized into a fixed absolute
449
     * timestamp.
450
     *
451
     * @param int $time
452
     *
453
     * @return int
454
     */
455 View Code Duplication
    protected function normalizeTime($time)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
456
    {
457
        // 0 = infinity
458
        if (!$time) {
459
            return 0;
460
        }
461
462
        // relative time in seconds, <30 days
463
        if ($time < 30 * 24 * 60 * 60) {
464
            $time += time();
465
        }
466
467
        return $time;
468
    }
469
470
    /**
471
     * Build value, token & expiration time to be stored in cache file.
472
     *
473
     * @param string $value
474
     * @param int    $expire
475
     *
476
     * @return string
477
     */
478
    protected function wrap($value, $expire)
479
    {
480
        $expire = $this->normalizeTime($expire);
481
482
        return $expire."\n".serialize($value);
483
    }
484
485
    /**
486
     * Fetch stored data from cache file.
487
     *
488
     * @param string $key
489
     *
490
     * @return bool|array
491
     */
492
    protected function read($key)
493
    {
494
        $path = $this->path($key);
495
        try {
496
            $data = $this->filesystem->read($path);
497
        } catch (FileNotFoundException $e) {
498
            // unlikely given previous 'exists' check, but let's play safe...
499
            // (outside process may have removed it since)
500
            return false;
501
        }
502
503
        if ($data === false) {
504
            // in theory, a file could still be deleted between Flysystem's
505
            // assertPresent & the time it actually fetched the content
506
            // extremely unlikely though
507
            return false;
508
        }
509
510
        $data = explode("\n", $data, 2);
511
        $data[0] = (int) $data[0];
512
513
        return $data;
514
    }
515
516
    /**
517
     * @param string $key
518
     *
519
     * @return string
520
     */
521
    protected function path($key)
522
    {
523
        return $key.'.cache';
524
    }
525
}
526