Completed
Push — master ( 4e8206...51bdf3 )
by Peter
02:16
created

PhpCache   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 338
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 77.86%

Importance

Changes 8
Bugs 2 Features 0
Metric Value
wmc 50
c 8
b 2
f 0
lcom 1
cbo 8
dl 0
loc 338
ccs 102
cts 131
cp 0.7786
rs 8.6206

14 Methods

Rating   Name   Duplication   Size   Complexity  
A set() 0 12 1
A remove() 0 11 2
A getCacheKey() 0 12 2
A classToFile() 0 4 1
A mkdir() 0 6 1
B __construct() 0 29 5
A setComponent() 0 6 1
A setOptions() 0 5 1
D prepare() 0 36 10
C get() 0 50 10
A clear() 0 5 1
A clearCurrentPath() 0 5 1
C clearPath() 0 30 8
B getFilename() 0 33 6

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
/*
4
 * To change this license header, choose License Headers in Project Properties.
5
 * To change this template file, choose Tools | Templates
6
 * and open the template in the editor.
7
 */
8
9
namespace Maslosoft\Addendum\Cache;
10
11
use DirectoryIterator;
12
use Maslosoft\Addendum\Addendum;
13
use Maslosoft\Addendum\Helpers\SoftIncluder;
14
use Maslosoft\Addendum\Interfaces\AnnotatedInterface;
15
use Maslosoft\Addendum\Options\MetaOptions;
16
use Maslosoft\Addendum\Utilities\ClassChecker;
17
use Maslosoft\Addendum\Utilities\NameNormalizer;
18
use Maslosoft\Cli\Shared\ConfigDetector;
19
use Maslosoft\Cli\Shared\Helpers\PhpExporter;
20
use ReflectionClass;
21
use RuntimeException;
22
use UnexpectedValueException;
23
24
/**
25
 * PhpCache
26
 *
27
 * @author Piotr Maselkowski <pmaselkowski at gmail.com>
28
 */
29
abstract class PhpCache
30
{
31
32
	private $metaClass = null;
33
34
	/**
35
	 *
36
	 * @var AnnotatedInterface|object|string
37
	 */
38
	private $component = null;
39
40
	/**
41
	 * Options
42
	 * @var string
43
	 */
44
	private $instanceId = null;
45
46
	/**
47
	 * Addendum runtime path
48
	 * @var string
49
	 */
50
	private $path = '';
51
52
	/**
53
	 *
54
	 * @var NsCache
55
	 */
56
	private $nsCache = null;
57
58
	/**
59
	 *
60
	 * @var Addendum
61
	 */
62
	private $addendum = null;
63
64
	/**
65
	 * Runtime path
66
	 * @var string
67
	 */
68
	private static $runtimePath = null;
69
70
	/**
71
	 * Local cacheq
72
	 * @var type
73
	 */
74
	private static $cache = [];
75
76
	/**
77
	 * Hash map of prepared directories
78
	 * Key is directory, value is flag indicating if it's prepared.
79
	 * @var bool[]
80
	 */
81
	private static $prepared = [];
82
	private $fileName = null;
83
84
	/**
85
	 *
86
	 * @param string $metaClass
87
	 * @param AnnotatedInterface|object|string $component
88
	 * @param MetaOptions|Addendum $options
89
	 */
90 56
	public function __construct($metaClass = null, $component = null, $options = null)
91
	{
92 56
		if (null === self::$runtimePath)
93
		{
94 1
			self::$runtimePath = (new ConfigDetector)->getRuntimePath();
95
		}
96 56
		$this->path = self::$runtimePath . '/addendum';
97 56
		$this->metaClass = $metaClass;
98 56
		$this->component = $component;
99 56
		if (empty($options))
100
		{
101 4
			$this->instanceId = Addendum::DefaultInstanceId;
102
		}
103 54
		elseif ($options instanceof Addendum)
104
		{
105 54
			$this->instanceId = $options->getInstanceId();
106
		}
107
		elseif ($options instanceof MetaOptions)
108
		{
109
			$this->instanceId = $options->instanceId;
110
		}
111
		else
112
		{
113
			throw new UnexpectedValueException('Unknown options');
114
		}
115 56
		$this->prepare();
116 56
		$this->addendum = Addendum::fly($this->instanceId);
117 56
		$this->nsCache = new NsCache(dirname($this->getFilename()), $this->addendum);
118 56
	}
119
120
	/**
121
	 * Set working component
122
	 * @param AnnotatedInterface|object|string $component
123
	 */
124 54
	public function setComponent($component = null)
125
	{
126
		// Reset filename as it depends on component
127 54
		$this->fileName = null;
128 54
		$this->component = $component;
129 54
	}
130
131 33
	public function setOptions(MetaOptions $options = null)
132
	{
133 33
		$this->fileName = null;
134 33
		$this->nsCache->setOptions($options);
135 33
	}
136
137 56
	private function prepare()
138
	{
139 56
		$fileDir = dirname($this->getFilename());
140 56
		if (isset(self::$prepared[$fileDir]) && self::$prepared[$fileDir])
141
		{
142 54
			return true;
143
		}
144 6
		if (!file_exists($this->path))
145
		{
146 1
			if (!file_exists(self::$runtimePath))
147
			{
148
149
				if (is_writable(dirname(self::$runtimePath)))
150
				{
151
					$this->mkdir(self::$runtimePath);
152
				}
153
				if (!is_writable(self::$runtimePath))
154
				{
155
					throw new RuntimeException(sprintf("Runtime path `%s` must exists and be writable", self::$runtimePath));
156
				}
157
			}
158 1
			if (is_writable(self::$runtimePath))
159
			{
160 1
				$this->mkdir($this->path);
161
			}
162 1
			if (!is_writable($this->path))
163
			{
164
				throw new RuntimeException(sprintf("Addendum runtime path `%s` must exists and be writable", $this->path));
165
			}
166
		}
167 6
		if (!file_exists($fileDir))
168
		{
169 2
			$this->mkdir($fileDir);
170
		}
171 6
		self::$prepared[$fileDir] = true;
172 6
	}
173
174 54
	public function get()
175
	{
176 54
		$this->prepare();
177 54
		$fileName = $this->getFilename();
178
179 54
		if (NsCache::$addeNs && !$this->nsCache->valid())
180
		{
181 1
			$this->clearCurrentPath();
182 1
			return false;
183
		}
184 54
		$key = $this->getCacheKey();
185 54
		if (isset(self::$cache[$key]))
186
		{
187 54
			return self::$cache[$key];
188
		}
189
190 52
		$data = SoftIncluder::includeFile($fileName);
191
192
		// Only false means not existing cache.
193
		// NOTE: Cache might have valid `empty` value, ie. empty array.
194 52
		if (false === $data)
195
		{
196 52
			return false;
197
		}
198
199
		// Purge file cache if checkMTime is enabled and file obsolete
200
		if ($this->addendum->checkMTime && file_exists($fileName))
201
		{
202
			$cacheTime = filemtime($fileName);
203
204
			// Partial component name, split by @ and take first argument
205
			if (is_string($this->component) && strstr($this->component, '@'))
206
			{
207
				$parts = explode('@', $this->component);
208
				$componentClass = array_shift($parts);
209
			}
210
			else
211
			{
212
				$componentClass = $this->component;
213
			}
214
			$componentTime = filemtime((new ReflectionClass($componentClass))->getFileName());
215
			if ($componentTime > $cacheTime)
216
			{
217
				$this->remove();
218
				return false;
219
			}
220
		}
221
		self::$cache[$key] = $data;
222
		return $data;
223
	}
224
225 52
	public function set($data)
226
	{
227 52
		$fileName = $this->getFilename();
228 52
		$this->prepare();
229 52
		$key = $this->getCacheKey();
230 52
		self::$cache[$key] = $data;
231
232 52
		file_put_contents($fileName, PhpExporter::export($data));
233 52
		@chmod($fileName, 0666);
234 52
		$this->nsCache->set();
235 52
		return $data;
236
	}
237
238
	public function remove()
239
	{
240
		$fileName = $this->getFilename();
241
		$key = $this->getCacheKey();
242
		unset(self::$cache[$key]);
243
		if (file_exists($fileName))
244
		{
245
			return unlink($fileName);
246
		}
247
		return false;
248
	}
249
250
	/**
251
	 * Clear entire cache
252
	 * @return boolean
253
	 */
254 3
	public function clear()
255
	{
256 3
		self::$prepared = [];
257 3
		return $this->clearPath($this->path);
258
	}
259
260 1
	private function clearCurrentPath()
261
	{
262 1
		$path = dirname($this->getFilename());
263 1
		return $this->clearPath($path);
264
	}
265
266 4
	private function clearPath($path)
267
	{
268 4
		if (!file_exists($path))
269
		{
270
			return false;
271
		}
272 4
		foreach (new DirectoryIterator($path) as $dir)
273
		{
274 4
			if ($dir->isDot() || !$dir->isDir())
275
			{
276 4
				continue;
277
			}
278 3
			foreach (new DirectoryIterator($dir->getPathname()) as $file)
279
			{
280 3
				if (!$file->isFile())
281
				{
282 3
					continue;
283
				}
284
285
				// Skip ns cache file, or it might regenerate over and over
286
				// ns file cache is replaced when needed by NsCache
287 3
				if ($file->getBasename() === NsCache::FileName)
288
				{
289 3
					continue;
290
				}
291 3
				unlink($file->getPathname());
292
			}
293
		}
294 4
		self::$prepared[$path] = false;
295 4
	}
296
297 56
	private function getFilename()
298
	{
299 56
		if (!empty($this->fileName))
300
		{
301 56
			return $this->fileName;
302
		}
303 56
		if (is_object($this->component))
304
		{
305 32
			$className = get_class($this->component);
306
		}
307 56
		elseif (is_string($this->component) || null === $this->component)
308
		{
309 56
			$className = $this->component;
310
		}
311
		else
312
		{
313
			throw new UnexpectedValueException(sprintf('Expected string or object or null got: `%s`', gettype($className)));
314
		}
315
316 56
		if (ClassChecker::isAnonymous($className))
317
		{
318 1
			NameNormalizer::normalize($className);
319
		}
320
321
		$params = [
322 56
			(string) $this->path,
323 56
			(string) $this->classToFile($this->metaClass),
324 56
			(string) $this->instanceId,
325 56
			(string) str_replace('\\', '/', $this->classToFile($className))
326
		];
327 56
		$this->fileName = vsprintf('%s/%s@%s/%s.php', $params);
328 56
		return $this->fileName;
329
	}
330
331 54
	private function getCacheKey()
332
	{
333 54
		if (is_object($this->component))
334
		{
335 32
			$className = get_class($this->component);
336
		}
337
		else
338
		{
339 54
			$className = $this->component;
340
		}
341 54
		return sprintf('%s@%s@%s@%s.php', $this->classToFile(static::class), $this->classToFile($this->metaClass), $this->instanceId, str_replace('\\', '/', $this->classToFile($className)));
342
	}
343
344
	/**
345
	 * Convert slash separated class name to dot separated name.
346
	 * @param string $className
347
	 * @return string
348
	 */
349 56
	private function classToFile($className)
350
	{
351 56
		return str_replace('\\', '.', $className);
352
	}
353
354
	/**
355
	 * Recursively create dir with proper permissions.
356
	 *
357
	 * @param string $path
358
	 */
359 2
	private function mkdir($path)
360
	{
361 2
		$mask = umask(0000);
362 2
		mkdir($path, 0777, true);
363 2
		umask($mask);
364 2
	}
365
366
}
367