Completed
Push — master ( fbd4ac...e6a0d8 )
by Peter
21:07
created

PhpCache::__construct()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 5.4439

Importance

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