Completed
Push — master ( ab26f3...439ddb )
by Peter
09:17
created

PhpCache::prepare()   D

Complexity

Conditions 9
Paths 23

Size

Total Lines 36
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 55.8693

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 36
ccs 3
cts 18
cp 0.1666
rs 4.909
cc 9
eloc 17
nc 23
nop 0
crap 55.8693
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 23
	public function __construct($metaClass = null, $component = null, $options = null)
86
	{
87 23
		if (null === self::$runtimePath)
88
		{
89 1
			self::$runtimePath = (new ConfigDetector)->getRuntimePath();
90
		}
91 23
		$this->path = self::$runtimePath . '/addendum';
92 23
		$this->metaClass = $metaClass;
93 23
		$this->component = $component;
94 23
		if (empty($options))
95
		{
96 3
			$this->instanceId = Addendum::DefaultInstanceId;
97
		}
98
		elseif ($options instanceof Addendum)
99
		{
100 20
			$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
111 23
		$this->addendum = Addendum::fly($this->instanceId);
112 23
		$this->nsCache = new NsCache(dirname($this->getFilename()), $this->addendum);
113 23
	}
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 30
	public function setOptions(MetaOptions $options = null)
127
	{
128 30
		$this->nsCache->setOptions($options);
129 30
	}
130
131 50
	private function prepare()
132
	{
133 50
		$fileDir = dirname($this->getFilename());
134 50
		if (self::$prepared[$fileDir])
135
		{
136
			return true;
137
		}
138
		if (!file_exists($this->path))
139
		{
140
			if (!file_exists(self::$runtimePath))
141
			{
142
143
				if (is_writable(dirname(self::$runtimePath)))
144
				{
145
					$this->mkdir(self::$runtimePath);
146
				}
147
				if (!is_writable(self::$runtimePath))
148
				{
149
					throw new RuntimeException(sprintf("Runtime path `%s` must exists and be writable", self::$runtimePath));
150
				}
151
			}
152
			if (is_writable(self::$runtimePath))
153
			{
154
				$this->mkdir($this->path);
155
			}
156
			if (!is_writable($this->path))
157
			{
158
				throw new RuntimeException(sprintf("Addendum runtime path `%s` must exists and be writable", $this->path));
159
			}
160
		}
161
		if (!file_exists($fileDir))
162
		{
163
			$this->mkdir($fileDir);
164
		}
165
		self::$prepared[$fileDir] = true;
166
	}
167
168 50
	public function get()
169
	{
170 50
		$this->prepare();
171
		$fileName = $this->getFilename();
172
173
		if (!$this->nsCache->valid())
174
		{
175
			$this->clearCurrentPath();
176
			return false;
177
		}
178
		$key = $this->getCacheKey();
179
		if (isset(self::$cache[$key]))
180
		{
181
			return self::$cache[$key];
182
		}
183
184
		$data = SoftIncluder::includeFile($fileName);
185
186
		if (empty($data))
187
		{
188
			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
	public function set($data)
218
	{
219
		$fileName = $this->getFilename();
220
		$this->prepare();
221
		$key = $this->getCacheKey();
222
		self::$cache[$key] = $data;
223
224
		file_put_contents($fileName, PhpExporter::export($data));
225
		@chmod($fileName, 0666);
226
		$this->nsCache->set();
227
		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
		return $this->clearPath($this->path);
249
	}
250
251
	private function clearCurrentPath()
252
	{
253
		return $this->clearPath(dirname($this->getFilename()));
254
	}
255
256 3
	private function clearPath($path)
257
	{
258 3
		if (!file_exists($path))
259
		{
260 3
			return false;
261
		}
262
		foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $dir)
263
		{
264
			$dir->isDir() && !$dir->isLink() ? rmdir($dir->getPathname()) : unlink($dir->getPathname());
265
		}
266
		return rmdir($path);
267
	}
268
269 52
	private function getFilename()
270
	{
271 52
		if (!empty($this->fileName))
272
		{
273
			return $this->fileName;
274
		}
275 52
		if (is_object($this->component))
276
		{
277 30
			$className = get_class($this->component);
278
		}
279
		else
280
		{
281 23
			$className = $this->component;
282
		}
283 52
		$this->fileName = sprintf('%s/%s@%s/%s.php', $this->path, $this->classToFile($this->metaClass), $this->instanceId, str_replace('\\', '/', $this->classToFile($className)));
284 52
		return $this->fileName;
285
	}
286
287
	private function getCacheKey()
288
	{
289
		if (is_object($this->component))
290
		{
291
			$className = get_class($this->component);
292
		}
293
		else
294
		{
295
			$className = $this->component;
296
		}
297
		return sprintf('%s@%s@%s@%s.php', $this->classToFile(static::class), $this->classToFile($this->metaClass), $this->instanceId, str_replace('\\', '/', $this->classToFile($className)));
298
	}
299
300
	/**
301
	 * Convert slash separated class name to dot separated name.
302
	 * @param string $className
303
	 * @return string
304
	 */
305 52
	private function classToFile($className)
306
	{
307 52
		return str_replace('\\', '.', $className);
308
	}
309
310
	/**
311
	 * Recursively create dir with proper permissions.
312
	 *
313
	 * @param string $path
314
	 */
315
	private function mkdir($path)
316
	{
317
		$mask = umask(0000);
318
		mkdir($path, 0777, true);
319
		umask($mask);
320
	}
321
322
}
323