Filesystem   A
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 201
Duplicated Lines 4.98 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 34
lcom 1
cbo 1
dl 10
loc 201
c 0
b 0
f 0
rs 9.68

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 3
A get() 0 8 2
A set() 5 18 3
A delete() 5 10 3
A clear() 0 9 3
A garbageCollect() 0 13 2
A getRoot() 0 4 1
A getPath() 0 17 3
A getLock() 0 24 5
A releaseLock() 0 5 3
A rmDir() 0 14 6

How to fix   Duplicated Code   

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:

1
<?php
2
3
/**
4
 * Phoole (PHP7.2+)
5
 *
6
 * @category  Library
7
 * @package   Phoole\Base
8
 * @copyright Copyright (c) 2019 Hong Zhang
9
 */
10
declare(strict_types=1);
11
12
namespace Phoole\Base\Storage;
13
14
use Phoole\Base\Exception\NotFoundException;
15
16
/**
17
 * Storage using filesystem
18
 *
19
 * Good for cache / session etc.
20
 *
21
 * @package Phoole\Base
22
 */
23
class Filesystem implements StorageInterface
24
{
25
    /**
26
     * @var string
27
     */
28
    protected $rootPath;
29
30
    /**
31
     * normally 0 - 3
32
     *
33
     * @var int
34
     */
35
    protected $hashLevel;
36
37
    /**
38
     * @param  string $rootPath   the base/root storage directory
39
     * @param  int    $hashLevel  directory hash depth
40
     * @throws       \RuntimeException  if mkdir failed
41
     */
42
    public function __construct(
43
        string $rootPath,
44
        int $hashLevel = 2
45
    ) {
46
        if (!file_exists($rootPath) && !@mkdir($rootPath, 0777, TRUE)) {
47
            throw new \RuntimeException("Failed to create $rootPath");
48
        }
49
        $this->rootPath = realpath($rootPath) . \DIRECTORY_SEPARATOR;
50
        $this->hashLevel = $hashLevel;
51
    }
52
53
    /**
54
     * {@inheritDoc}
55
     */
56
    public function get(string $key): array
57
    {
58
        $path = $this->getPath($key);
59
        if (file_exists($path)) {
60
            return [file_get_contents($path), filemtime($path)];
61
        }
62
        throw new NotFoundException("Not found for $key");
63
    }
64
65
    /**
66
     * {@inheritDoc}
67
     */
68
    public function set(string $key, string $value, int $ttl): bool
69
    {
70
        $path = $this->getPath($key);
71
72
        // write to a temp file first
73
        $temp = tempnam(dirname($path), 'TMP');
74
        file_put_contents($temp, $value);
75
76
        // rename the temp file with locking
77 View Code Duplication
        if ($fp = $this->getLock($path)) {
78
            $res = rename($temp, $path) && touch($path, time() + $ttl);
79
            $this->releaseLock($path, $fp);
80
            return $res;
81
        }
82
83
        // locking failed
84
        return FALSE;
85
    }
86
87
    /**
88
     * {@inheritDoc}
89
     */
90
    public function delete(string $key): bool
91
    {
92
        $path = $this->getPath($key);
93 View Code Duplication
        if (file_exists($path) && $fp = $this->getLock($path)) {
94
            $res = unlink($path);
95
            $this->releaseLock($path, $fp);
96
            return $res;
97
        }
98
        return FALSE;
99
    }
100
101
    /**
102
     * {@inheritDoc}
103
     */
104
    public function clear(): bool
105
    {
106
        $path = rtrim($this->getRoot(), '/\\');
107
        if (file_exists($path)) {
108
            $temp = $path . '_' . substr(md5($path . microtime(TRUE)), -5);
109
            return rename($path, $temp) && mkdir($path, 0777, TRUE);
110
        }
111
        return FALSE;
112
    }
113
114
    /**
115
     * {@inheritDoc}
116
     */
117
    public function garbageCollect()
118
    {
119
        $root = $this->getRoot();
120
121
        // remove staled file
122
        $this->rmDir($root, TRUE);
123
124
        // remove staled directory
125
        $pattern = rtrim($root, '/\\') . '_*';
126
        foreach (glob($pattern, GLOB_MARK) as $dir) {
127
            $this->rmDir($dir);
128
        }
129
    }
130
131
    /**
132
     * @return string
133
     */
134
    protected function getRoot(): string
135
    {
136
        return $this->rootPath;
137
    }
138
139
    /**
140
     * @param  string $key
141
     * @return string
142
     */
143
    protected function getPath(string $key): string
144
    {
145
        // get hashed directory
146
        $name = $key . '00';
147
        $path = $this->getRoot();
148
        for ($i = 0; $i < $this->hashLevel; ++$i) {
149
            $path .= $name[$i] . \DIRECTORY_SEPARATOR;
150
        }
151
152
        // make sure hashed directory exists
153
        if (!file_exists($path)) {
154
            mkdir($path, 0777, TRUE);
155
        }
156
157
        // return full path
158
        return $path . $key;
159
    }
160
161
    /**
162
     * get lock of the path
163
     *
164
     * @param  string $path
165
     * @return resource|false
166
     */
167
    protected function getLock(string $path)
168
    {
169
        $lock = $path . '.lock';
170
        $count = 0;
171
        if ($fp = fopen($lock, 'c')) {
172
            while (TRUE) {
173
                // try 3 times only
174
                if ($count++ > 3) {
175
                    break;
176
                }
177
178
                // get lock non-blockingly
179
                if (flock($fp, \LOCK_EX|\LOCK_NB)) {
180
                    return $fp;
181
                }
182
183
                // sleep randon time between attempts
184
                usleep(rand(1, 10));
185
            }
186
            // failed
187
            fclose($fp);
188
        }
189
        return FALSE;
190
    }
191
192
    /**
193
     * @param  string   $path
194
     * @param  resource $fp
195
     * @return bool
196
     */
197
    protected function releaseLock(string $path, $fp): bool
198
    {
199
        $lock = $path . '.lock';
200
        return flock($fp, LOCK_UN) && fclose($fp) && unlink($lock);
201
    }
202
203
    /**
204
     * Remove a whole directory or stale files only
205
     *
206
     * @param  string $dir
207
     * @return void
208
     */
209
    protected function rmDir(string $dir, bool $staleOnly = FALSE)
210
    {
211
        foreach (glob($dir . '{,.}[!.,!..]*', GLOB_MARK|GLOB_BRACE) as $file) {
212
            if (is_dir($file)) {
213
                $this->rmDir($file, $staleOnly);
214
            } else {
215
                if ($staleOnly && filemtime($file) > time()) {
216
                    continue;
217
                }
218
                unlink($file);
219
            }
220
        }
221
        $staleOnly || rmdir($dir);
222
    }
223
}
224