Completed
Push — master ( 49a630...afaeb1 )
by Peter
02:50 queued 48s
created

PhpCache::__construct()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5.3256

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 13
cts 17
cp 0.7647
rs 9.1448
c 0
b 0
f 0
cc 5
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 cache
78
	 * @var array
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 60
	public function __construct($metaClass = null, $component = null, $options = null)
97
	{
98 60
		if (null === self::$runtimePath)
99
		{
100
			self::$runtimePath = (new ConfigDetector)->getRuntimePath();
101
		}
102 60
		$this->path = self::$runtimePath . '/addendum';
103 60
		$this->metaClass = $metaClass;
104 60
		$this->component = $component;
105 60
		if (empty($options))
106
		{
107 3
			$this->instanceId = Addendum::DefaultInstanceId;
108
		}
109 58
		elseif ($options instanceof Addendum)
110
		{
111 58
			$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 60
		$this->prepare();
122 60
		$this->addendum = Addendum::fly($this->instanceId);
123 60
		$this->nsCache = new NsCache(dirname($this->getFilename()), $this->addendum);
124 60
	}
125
126
	/**
127
	 * Set working component
128
	 * @param AnnotatedInterface|object|string $component
129
	 */
130 58
	public function setComponent($component = null)
131
	{
132
		// Reset filename as it depends on component
133 58
		$this->fileName = null;
134 58
		$this->component = $component;
135 58
	}
136
137
	public function setOptions(MetaOptions $options = null)
138
	{
139
		$this->fileName = null;
140
		$this->nsCache->setOptions($options);
141
	}
142
143
	/**
144
	 * Prepare cache storage
145
	 * @return bool
146
	 */
147 60
	private function prepare(): bool
148
	{
149 60
		$fileDir = dirname($this->getFilename());
150 60
		if (isset(self::$prepared[$fileDir]) && self::$prepared[$fileDir])
151
		{
152 58
			return true;
153
		}
154 60
		if (!file_exists($this->path))
155
		{
156
			if (!file_exists(self::$runtimePath))
157
			{
158
159
				if (is_writable(dirname(self::$runtimePath)))
160
				{
161
					$this->mkdir(self::$runtimePath);
162
				}
163
				if (!is_writable(self::$runtimePath))
164
				{
165
					throw new RuntimeException(sprintf("Runtime path `%s` must exists and be writable", self::$runtimePath));
166
				}
167
			}
168
			if (is_writable(self::$runtimePath))
169
			{
170
				$this->mkdir($this->path);
171
			}
172
			if (!is_writable($this->path))
173
			{
174
				throw new RuntimeException(sprintf("Addendum runtime path `%s` must exists and be writable", $this->path));
175
			}
176
		}
177 60
		if (!file_exists($fileDir))
178
		{
179 1
			$this->mkdir($fileDir);
180
		}
181 60
		self::$prepared[$fileDir] = true;
182 60
		return false;
183
	}
184
185 58
	public function get()
186
	{
187 58
		$this->prepare();
188 58
		$fileName = $this->getFilename();
189
190 58
		if (NsCache::$addedNs && !$this->nsCache->valid())
191
		{
192
			$this->clearCurrentPath();
193
			return false;
194
		}
195 58
		$key = $this->getCacheKey();
196 58
		if (isset(self::$cache[$key]))
197
		{
198 58
			return self::$cache[$key];
199
		}
200
201 53
		$data = SoftIncluder::includeFile($fileName);
202
203
		// Only false means not existing cache.
204
		// NOTE: Cache might have valid `empty` value, ie. empty array.
205 53
		if (false === $data)
206
		{
207 53
			return false;
208
		}
209
210
		// Purge file cache if checkMTime is enabled and file obsolete
211
		if ($this->addendum->checkMTime && file_exists($fileName))
212
		{
213
			$cacheTime = filemtime($fileName);
214
215
			// Partial component name, split by @ and take first argument
216
			if (is_string($this->component) && strstr($this->component, '@'))
217
			{
218
				$parts = explode('@', $this->component);
219
				$componentClass = array_shift($parts);
220
			}
221
			else
222
			{
223
				$componentClass = $this->component;
224
			}
225
			$componentTime = filemtime((new ReflectionClass($componentClass))->getFileName());
226
			if ($componentTime > $cacheTime)
227
			{
228
				$this->remove();
229
				return false;
230
			}
231
		}
232
		self::$cache[$key] = $data;
233
		return $data;
234
	}
235
236 53
	public function set($data)
237
	{
238 53
		$fileName = $this->getFilename();
239 53
		$this->prepare();
240 53
		$key = $this->getCacheKey();
241 53
		self::$cache[$key] = $data;
242
243 53
		file_put_contents($fileName, PhpExporter::export($data));
244 53
		@chmod($fileName, 0666);
245 53
		$this->nsCache->set();
246 53
		return $data;
247
	}
248
249
	/**
250
	 * @return bool
251
	 */
252
	public function remove(): bool
253
	{
254
		$fileName = $this->getFilename();
255
		$key = $this->getCacheKey();
256
		unset(self::$cache[$key]);
257
		if (file_exists($fileName))
258
		{
259
			return unlink($fileName);
260
		}
261
		return false;
262
	}
263
264
	/**
265
	 * Clear entire cache
266
	 * @return boolean
267
	 */
268 3
	public function clear(): bool
269
	{
270 3
		self::$prepared = [];
271 3
		return $this->clearPath($this->path);
272
	}
273
274
	/**
275
	 * @return bool
276
	 */
277
	private function clearCurrentPath(): bool
278
	{
279
		$path = dirname($this->getFilename());
280
		return $this->clearPath($path);
281
	}
282
283
	/**
284
	 * @param string $path
285
	 * @return bool
286
	 */
287 3
	private function clearPath(string $path): bool
288
	{
289 3
		if (!file_exists($path))
290
		{
291
			return false;
292
		}
293 3
		foreach (new DirectoryIterator($path) as $dir)
294
		{
295 3
			if ($dir->isDot() || !$dir->isDir())
296
			{
297 3
				continue;
298
			}
299 3
			foreach (new DirectoryIterator($dir->getPathname()) as $file)
300
			{
301 3
				if (!$file->isFile())
302
				{
303 3
					continue;
304
				}
305
306
				// Skip ns cache file, or it might regenerate over and over
307
				// ns file cache is replaced when needed by NsCache
308 3
				if ($file->getBasename() === NsCache::FileName)
309
				{
310 3
					continue;
311
				}
312 3
				unlink($file->getPathname());
313
			}
314
		}
315 3
		self::$prepared[$path] = false;
316 3
		return true;
317
	}
318
319 60
	private function getFilename()
320
	{
321 60
		if (!empty($this->fileName))
322
		{
323 60
			return $this->fileName;
324
		}
325 60
		if (is_object($this->component))
326
		{
327
			$className = get_class($this->component);
328
		}
329 60
		elseif (is_string($this->component) || null === $this->component)
330
		{
331 60
			$className = $this->component;
332
		}
333
		else
334
		{
335
			throw new UnexpectedValueException(sprintf('Expected string or object or null got: `%s`', gettype($this->component)));
336
		}
337
338 60
		if (ClassChecker::isAnonymous($className))
339
		{
340 1
			NameNormalizer::normalize($className);
341
		}
342
343
		$params = [
344 60
			(string) $this->path,
345 60
			(string) $this->classToFile($this->metaClass),
346 60
			(string) $this->instanceId,
347 60
			(string) str_replace('\\', '/', $this->classToFile($className))
348
		];
349 60
		$this->fileName = vsprintf('%s/%s@%s/%s.php', $params);
350 60
		return $this->fileName;
351
	}
352
353 58
	private function getCacheKey()
354
	{
355 58
		if (is_object($this->component))
356
		{
357
			$className = get_class($this->component);
358
		}
359
		else
360
		{
361 58
			$className = $this->component;
362
		}
363 58
		return sprintf('%s@%s@%s@%s.php', $this->classToFile(static::class), $this->classToFile($this->metaClass), $this->instanceId, str_replace('\\', '/', $this->classToFile($className)));
364
	}
365
366
	/**
367
	 * Convert slash separated class name to dot separated name.
368
	 * @param string $className
369
	 * @return string
370
	 */
371 60
	private function classToFile($className)
372
	{
373 60
		return str_replace('\\', '.', $className);
374
	}
375
376
	/**
377
	 * Recursively create dir with proper permissions.
378
	 *
379
	 * @param string $path
380
	 */
381 1
	private function mkdir($path)
382
	{
383 1
		$mask = umask(0000);
384 1
		mkdir($path, 0777, true);
385 1
		umask($mask);
386 1
	}
387
388
}