PhpCache::setComponent()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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