Completed
Push — master ( 5cfb46...2936f6 )
by Lars
01:39
created

AdapterFile   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 298
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 2

Test Coverage

Coverage 86.41%

Importance

Changes 2
Bugs 0 Features 1
Metric Value
wmc 48
lcom 2
cbo 2
dl 0
loc 298
ccs 89
cts 103
cp 0.8641
rs 8.4864
c 2
b 0
f 1

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 3
C createCacheDirectory() 0 46 13
A deleteFile() 0 8 2
A exists() 0 6 1
D get() 0 44 10
A installed() 0 4 1
A remove() 0 6 1
A removeAll() 0 15 4
A set() 0 4 1
B setExpired() 0 24 4
A getFileName() 0 4 1
A setFileMode() 0 4 1
A ttlHasExpired() 0 8 2
A validateDataFromCache() 0 14 4

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\cache;
6
7
/**
8
 * AdapterFile: File-adapter
9
 *
10
 * @package voku\cache
11
 */
12
class AdapterFile implements iAdapter
13
{
14
  const CACHE_FILE_PREFIX = '__';
15
  const CACHE_FILE_SUBFIX = '.php.cache';
16
17
  /**
18
   * @var bool
19
   */
20
  public $installed = false;
21
22
  /**
23
   * @var string
24
   */
25
  protected $cacheDir;
26
27
  /**
28
   * @var iSerializer
29
   */
30
  protected $serializer;
31
32
  /**
33
   * @var string
34
   */
35
  protected $fileMode = '0755';
36
37
  /**
38
   * @param string $cacheDir
39
   */
40 11
  public function __construct($cacheDir = null)
41
  {
42 11
    $this->serializer = new SerializerIgbinary();
43
44 11
    if (!$cacheDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cacheDir of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
45 11
      $cacheDir = \realpath(\sys_get_temp_dir()) . '/simple_php_cache';
46
    }
47
48 11
    $this->cacheDir = (string)$cacheDir;
49
50 11
    if ($this->createCacheDirectory($cacheDir) === true) {
51 11
      $this->installed = true;
52
    }
53 11
  }
54
55
  /**
56
   * Recursively creates & chmod directories.
57
   *
58
   * @param string $path
59
   *
60
   * @return bool
61
   */
62 11
  protected function createCacheDirectory($path): bool
63
  {
64
    if (
65 11
        !$path
66
        ||
67 11
        $path === '/'
68
        ||
69 11
        $path === '.'
70
        ||
71 11
        $path === '\\'
72
    ) {
73
      return false;
74
    }
75
76
    // if the directory already exists, just return true
77 11
    if (\is_dir($path) && \is_writable($path)) {
78 11
      return true;
79
    }
80
81
    // if more than one level, try parent first
82 1
    if (\dirname($path) !== '.') {
83 1
      $return = $this->createCacheDirectory(\dirname($path));
84
      // if creating parent fails, we can abort immediately
85 1
      if (!$return) {
86
        return false;
87
      }
88
    }
89
90 1
    $mode_dec = \intval($this->fileMode, 8);
91 1
    $old_umask = \umask(0);
92
93
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
94 1
    if (!@\mkdir($path, $mode_dec) && !\is_dir($path)) {
95
      $return = false;
96
    } else {
97 1
      $return = true;
98
    }
99
100 1
    if (\is_dir($path) && !\is_writable($path)) {
101
      $return = \chmod($path, $mode_dec);
102
    }
103
104 1
    \umask($old_umask);
105
106 1
    return $return;
107
  }
108
109
  /**
110
   * @param $cacheFile
111
   *
112
   * @return bool
113
   */
114 3
  protected function deleteFile($cacheFile): bool
115
  {
116 3
    if (\is_file($cacheFile)) {
117 3
      return \unlink($cacheFile);
118
    }
119
120
    return false;
121
  }
122
123
  /**
124
   * @inheritdoc
125
   */
126 1
  public function exists(string $key): bool
127
  {
128 1
    $value = $this->get($key);
129
130 1
    return null !== $value;
131
  }
132
133
  /**
134
   * @inheritdoc
135
   */
136 7
  public function get(string $key)
137
  {
138 7
    $path = $this->getFileName($key);
139
140
    if (
141 7
        file_exists($path) === false
142
        ||
143 7
        filesize($path) === 0
144
    ) {
145 1
      return null;
146
    }
147
148
    // init
149 7
    $string = '';
150
151
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
152 7
    $fp = @fopen($path, 'rb');
153 7
    if ($fp && flock($fp, LOCK_SH | LOCK_NB)) {
154 7
      while (!feof($fp)) {
155 7
        $line = fgets($fp);
156 7
        $string .= $line;
157
      }
158 7
      flock($fp, LOCK_UN);
159
    }
160 7
    fclose($fp);
161
162 7
    if (!$string) {
163
      return null;
164
    }
165
166 7
    $data = $this->serializer->unserialize(file_get_contents($path));
167
168 7
    if (!$data || !$this->validateDataFromCache($data)) {
169
      return null;
170
    }
171
172 7
    if ($this->ttlHasExpired($data['ttl']) === true) {
173 2
      $this->remove($key);
174
175 2
      return null;
176
    }
177
178 6
    return $data['value'];
179
  }
180
181
  /**
182
   * @inheritdoc
183
   */
184
  public function installed(): bool
185
  {
186
    return $this->installed;
187
  }
188
189
  /**
190
   * @inheritdoc
191
   */
192 3
  public function remove(string $key): bool
193
  {
194 3
    $cacheFile = $this->getFileName($key);
195
196 3
    return $this->deleteFile($cacheFile);
197
  }
198
199
  /**
200
   * @inheritdoc
201
   */
202 1
  public function removeAll(): bool
203
  {
204 1
    if (!$this->cacheDir) {
205
      return false;
206
    }
207
208 1
    $return = array();
209 1
    foreach (new \DirectoryIterator($this->cacheDir) as $fileInfo) {
210 1
      if (!$fileInfo->isDot()) {
211 1
        $return[] = unlink($fileInfo->getPathname());
212
      }
213
    }
214
215 1
    return \in_array(false, $return, true) === false;
216
  }
217
218
  /**
219
   * @inheritdoc
220
   */
221 3
  public function set(string $key, $value): bool
222
  {
223 3
    return $this->setExpired($key, $value);
224
  }
225
226
  /**
227
   * @inheritdoc
228
   */
229 6
  public function setExpired(string $key, $value, int $ttl = 0): bool
230
  {
231 6
    $item = $this->serializer->serialize(
232
        array(
233 6
            'value' => $value,
234 6
            'ttl'   => $ttl ? (int)$ttl + time() : 0,
235
        )
236
    );
237
238 6
    $octetWritten = false;
239 6
    $cacheFile = $this->getFileName($key);
240
241
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
242 6
    $fp = @fopen($cacheFile, 'ab');
243 6
    if ($fp && flock($fp, LOCK_EX | LOCK_NB)) {
244 6
      ftruncate($fp, 0);
245 6
      $octetWritten = fwrite($fp, $item);
246 6
      fflush($fp);
247 6
      flock($fp, LOCK_UN);
248
    }
249 6
    fclose($fp);
250
251 6
    return $octetWritten !== false;
252
  }
253
254
  /**
255
   * @param string $key
256
   *
257
   * @return string
258
   */
259 8
  protected function getFileName(string $key): string
260
  {
261 8
    return $this->cacheDir . DIRECTORY_SEPARATOR . self::CACHE_FILE_PREFIX . $key . self::CACHE_FILE_SUBFIX;
262
  }
263
264
  /**
265
   * Set the file-mode for new cache-files.
266
   *
267
   * e.g. '0777', or '0755' ...
268
   *
269
   * @param $fileMode
270
   */
271
  public function setFileMode($fileMode)
272
  {
273
    $this->fileMode = $fileMode;
274
  }
275
276
  /**
277
   * @param $ttl
278
   *
279
   * @return bool
280
   */
281 7
  protected function ttlHasExpired(int $ttl): bool
282
  {
283 7
    if ($ttl === 0) {
284 4
      return false;
285
    }
286
287 3
    return (time() > $ttl);
288
  }
289
290
  /**
291
   * @param mixed $data
292
   *
293
   * @return bool
294
   */
295 7
  protected function validateDataFromCache($data): bool
296
  {
297 7
    if (!\is_array($data)) {
298
      return false;
299
    }
300
301 7
    foreach (array('value', 'ttl') as $missing) {
302 7
      if (!array_key_exists($missing, $data)) {
303 7
        return false;
304
      }
305
    }
306
307 7
    return true;
308
  }
309
}
310