Completed
Push — master ( 81f196...9f76bb )
by Peter
15:55
created

PhpCache::getCacheKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

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