/lib/scssphp/Cache.php

https://github.com/sbourget/moodle · PHP · 272 lines · 119 code · 44 blank · 109 comment · 21 complexity · 6d4a3ae427380479a435cf7c9848ffc9 MD5 · raw file

  1. <?php
  2. /**
  3. * SCSSPHP
  4. *
  5. * @copyright 2012-2020 Leaf Corcoran
  6. *
  7. * @license http://opensource.org/licenses/MIT MIT
  8. *
  9. * @link http://scssphp.github.io/scssphp
  10. */
  11. namespace ScssPhp\ScssPhp;
  12. use Exception;
  13. use ScssPhp\ScssPhp\Version;
  14. /**
  15. * The scss cache manager.
  16. *
  17. * In short:
  18. *
  19. * allow to put in cache/get from cache a generic result from a known operation on a generic dataset,
  20. * taking in account options that affects the result
  21. *
  22. * The cache manager is agnostic about data format and only the operation is expected to be described by string
  23. */
  24. /**
  25. * SCSS cache
  26. *
  27. * @author Cedric Morin <cedric@yterium.com>
  28. *
  29. * @internal
  30. */
  31. class Cache
  32. {
  33. const CACHE_VERSION = 1;
  34. /**
  35. * directory used for storing data
  36. *
  37. * @var string|false
  38. */
  39. public static $cacheDir = false;
  40. /**
  41. * prefix for the storing data
  42. *
  43. * @var string
  44. */
  45. public static $prefix = 'scssphp_';
  46. /**
  47. * force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit
  48. *
  49. * @var bool|string
  50. */
  51. public static $forceRefresh = false;
  52. /**
  53. * specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up
  54. *
  55. * @var int
  56. */
  57. public static $gcLifetime = 604800;
  58. /**
  59. * array of already refreshed cache if $forceRefresh==='once'
  60. *
  61. * @var array<string, bool>
  62. */
  63. protected static $refreshed = [];
  64. /**
  65. * Constructor
  66. *
  67. * @param array $options
  68. *
  69. * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string} $options
  70. */
  71. public function __construct($options)
  72. {
  73. // check $cacheDir
  74. if (isset($options['cacheDir'])) {
  75. self::$cacheDir = $options['cacheDir'];
  76. }
  77. if (empty(self::$cacheDir)) {
  78. throw new Exception('cacheDir not set');
  79. }
  80. if (isset($options['prefix'])) {
  81. self::$prefix = $options['prefix'];
  82. }
  83. if (empty(self::$prefix)) {
  84. throw new Exception('prefix not set');
  85. }
  86. if (isset($options['forceRefresh'])) {
  87. self::$forceRefresh = $options['forceRefresh'];
  88. }
  89. self::checkCacheDir();
  90. }
  91. /**
  92. * Get the cached result of $operation on $what,
  93. * which is known as dependant from the content of $options
  94. *
  95. * @param string $operation parse, compile...
  96. * @param mixed $what content key (e.g., filename to be treated)
  97. * @param array $options any option that affect the operation result on the content
  98. * @param int|null $lastModified last modified timestamp
  99. *
  100. * @return mixed
  101. *
  102. * @throws \Exception
  103. */
  104. public function getCache($operation, $what, $options = [], $lastModified = null)
  105. {
  106. $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
  107. if (
  108. ((self::$forceRefresh === false) || (self::$forceRefresh === 'once' &&
  109. isset(self::$refreshed[$fileCache]))) && file_exists($fileCache)
  110. ) {
  111. $cacheTime = filemtime($fileCache);
  112. if (
  113. (\is_null($lastModified) || $cacheTime > $lastModified) &&
  114. $cacheTime + self::$gcLifetime > time()
  115. ) {
  116. $c = file_get_contents($fileCache);
  117. $c = unserialize($c);
  118. if (\is_array($c) && isset($c['value'])) {
  119. return $c['value'];
  120. }
  121. }
  122. }
  123. return null;
  124. }
  125. /**
  126. * Put in cache the result of $operation on $what,
  127. * which is known as dependant from the content of $options
  128. *
  129. * @param string $operation
  130. * @param mixed $what
  131. * @param mixed $value
  132. * @param array $options
  133. *
  134. * @return void
  135. */
  136. public function setCache($operation, $what, $value, $options = [])
  137. {
  138. $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
  139. $c = ['value' => $value];
  140. $c = serialize($c);
  141. file_put_contents($fileCache, $c);
  142. if (self::$forceRefresh === 'once') {
  143. self::$refreshed[$fileCache] = true;
  144. }
  145. }
  146. /**
  147. * Get the cache name for the caching of $operation on $what,
  148. * which is known as dependant from the content of $options
  149. *
  150. * @param string $operation
  151. * @param mixed $what
  152. * @param array $options
  153. *
  154. * @return string
  155. */
  156. private static function cacheName($operation, $what, $options = [])
  157. {
  158. $t = [
  159. 'version' => self::CACHE_VERSION,
  160. 'scssphpVersion' => Version::VERSION,
  161. 'operation' => $operation,
  162. 'what' => $what,
  163. 'options' => $options
  164. ];
  165. $t = self::$prefix
  166. . sha1(json_encode($t))
  167. . ".$operation"
  168. . ".scsscache";
  169. return $t;
  170. }
  171. /**
  172. * Check that the cache dir exists and is writeable
  173. *
  174. * @return void
  175. *
  176. * @throws \Exception
  177. */
  178. public static function checkCacheDir()
  179. {
  180. self::$cacheDir = str_replace('\\', '/', self::$cacheDir);
  181. self::$cacheDir = rtrim(self::$cacheDir, '/') . '/';
  182. if (! is_dir(self::$cacheDir)) {
  183. throw new Exception('Cache directory doesn\'t exist: ' . self::$cacheDir);
  184. }
  185. if (! is_writable(self::$cacheDir)) {
  186. throw new Exception('Cache directory isn\'t writable: ' . self::$cacheDir);
  187. }
  188. }
  189. /**
  190. * Delete unused cached files
  191. *
  192. * @return void
  193. */
  194. public static function cleanCache()
  195. {
  196. static $clean = false;
  197. if ($clean || empty(self::$cacheDir)) {
  198. return;
  199. }
  200. $clean = true;
  201. // only remove files with extensions created by SCSSPHP Cache
  202. // css files removed based on the list files
  203. $removeTypes = ['scsscache' => 1];
  204. $files = scandir(self::$cacheDir);
  205. if (! $files) {
  206. return;
  207. }
  208. $checkTime = time() - self::$gcLifetime;
  209. foreach ($files as $file) {
  210. // don't delete if the file wasn't created with SCSSPHP Cache
  211. if (strpos($file, self::$prefix) !== 0) {
  212. continue;
  213. }
  214. $parts = explode('.', $file);
  215. $type = array_pop($parts);
  216. if (! isset($removeTypes[$type])) {
  217. continue;
  218. }
  219. $fullPath = self::$cacheDir . $file;
  220. $mtime = filemtime($fullPath);
  221. // don't delete if it's a relatively new file
  222. if ($mtime > $checkTime) {
  223. continue;
  224. }
  225. unlink($fullPath);
  226. }
  227. }
  228. }