Completed
Push — master ( 1de3af...702c50 )
by Peter
05:53
created

PhpCache::__construct()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5.3256

Importance

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