Completed
Push — master ( 93c108...b5b8be )
by Peter
07:59
created

PhpCache::clearPath()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5.0729

Importance

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