Cache.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. <?php
  2. /**
  3. * @package Grav.Common
  4. *
  5. * @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common;
  9. use \Doctrine\Common\Cache as DoctrineCache;
  10. use Grav\Common\Config\Config;
  11. use Grav\Common\Filesystem\Folder;
  12. use RocketTheme\Toolbox\Event\Event;
  13. /**
  14. * The GravCache object is used throughout Grav to store and retrieve cached data.
  15. * It uses DoctrineCache library and supports a variety of caching mechanisms. Those include:
  16. *
  17. * APCu
  18. * APC
  19. * XCache
  20. * RedisCache
  21. * MemCache
  22. * MemCacheD
  23. * FileSystem
  24. */
  25. class Cache extends Getters
  26. {
  27. /**
  28. * @var string Cache key.
  29. */
  30. protected $key;
  31. protected $lifetime;
  32. protected $now;
  33. /** @var Config $config */
  34. protected $config;
  35. /**
  36. * @var DoctrineCache\CacheProvider
  37. */
  38. protected $driver;
  39. protected $driver_name;
  40. protected $driver_setting;
  41. /**
  42. * @var bool
  43. */
  44. protected $enabled;
  45. protected $cache_dir;
  46. protected static $standard_remove = [
  47. 'cache://twig/',
  48. 'cache://doctrine/',
  49. 'cache://compiled/',
  50. 'cache://validated-',
  51. 'cache://images',
  52. 'asset://',
  53. ];
  54. protected static $standard_remove_no_images = [
  55. 'cache://twig/',
  56. 'cache://doctrine/',
  57. 'cache://compiled/',
  58. 'cache://validated-',
  59. 'asset://',
  60. ];
  61. protected static $all_remove = [
  62. 'cache://',
  63. 'cache://images',
  64. 'asset://',
  65. 'tmp://'
  66. ];
  67. protected static $assets_remove = [
  68. 'asset://'
  69. ];
  70. protected static $images_remove = [
  71. 'cache://images'
  72. ];
  73. protected static $cache_remove = [
  74. 'cache://'
  75. ];
  76. protected static $tmp_remove = [
  77. 'tmp://'
  78. ];
  79. /**
  80. * Constructor
  81. *
  82. * @param Grav $grav
  83. */
  84. public function __construct(Grav $grav)
  85. {
  86. $this->init($grav);
  87. }
  88. /**
  89. * Initialization that sets a base key and the driver based on configuration settings
  90. *
  91. * @param Grav $grav
  92. *
  93. * @return void
  94. */
  95. public function init(Grav $grav)
  96. {
  97. /** @var Config $config */
  98. $this->config = $grav['config'];
  99. $this->now = time();
  100. $this->cache_dir = $grav['locator']->findResource('cache://doctrine', true, true);
  101. /** @var Uri $uri */
  102. $uri = $grav['uri'];
  103. $prefix = $this->config->get('system.cache.prefix');
  104. if (is_null($this->enabled)) {
  105. $this->enabled = (bool)$this->config->get('system.cache.enabled');
  106. }
  107. // Cache key allows us to invalidate all cache on configuration changes.
  108. $this->key = ($prefix ? $prefix : 'g') . '-' . substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION),
  109. 2, 8);
  110. $this->driver_setting = $this->config->get('system.cache.driver');
  111. $this->driver = $this->getCacheDriver();
  112. // Set the cache namespace to our unique key
  113. $this->driver->setNamespace($this->key);
  114. }
  115. /**
  116. * Public accessor to set the enabled state of the cache
  117. *
  118. * @param $enabled
  119. */
  120. public function setEnabled($enabled)
  121. {
  122. $this->enabled = (bool) $enabled;
  123. }
  124. /**
  125. * Returns the current enabled state
  126. *
  127. * @return bool
  128. */
  129. public function getEnabled()
  130. {
  131. return $this->enabled;
  132. }
  133. /**
  134. * Get cache state
  135. *
  136. * @return string
  137. */
  138. public function getCacheStatus()
  139. {
  140. return 'Cache: [' . ($this->enabled ? 'true' : 'false') . '] Setting: [' . $this->driver_setting . '] Driver: [' . $this->driver_name . ']';
  141. }
  142. /**
  143. * Automatically picks the cache mechanism to use. If you pick one manually it will use that
  144. * If there is no config option for $driver in the config, or it's set to 'auto', it will
  145. * pick the best option based on which cache extensions are installed.
  146. *
  147. * @return DoctrineCache\CacheProvider The cache driver to use
  148. */
  149. public function getCacheDriver()
  150. {
  151. $setting = $this->driver_setting;
  152. $driver_name = 'file';
  153. // CLI compatibility requires a non-volatile cache driver
  154. if ($this->config->get('system.cache.cli_compatibility') && (
  155. $setting == 'auto' || $this->isVolatileDriver($setting))) {
  156. $setting = $driver_name;
  157. }
  158. if (!$setting || $setting == 'auto') {
  159. if (extension_loaded('apcu')) {
  160. $driver_name = 'apcu';
  161. } elseif (extension_loaded('apc')) {
  162. $driver_name = 'apc';
  163. } elseif (extension_loaded('wincache')) {
  164. $driver_name = 'wincache';
  165. } elseif (extension_loaded('xcache')) {
  166. $driver_name = 'xcache';
  167. }
  168. } else {
  169. $driver_name = $setting;
  170. }
  171. $this->driver_name = $driver_name;
  172. switch ($driver_name) {
  173. case 'apc':
  174. $driver = new DoctrineCache\ApcCache();
  175. break;
  176. case 'apcu':
  177. $driver = new DoctrineCache\ApcuCache();
  178. break;
  179. case 'wincache':
  180. $driver = new DoctrineCache\WinCacheCache();
  181. break;
  182. case 'xcache':
  183. $driver = new DoctrineCache\XcacheCache();
  184. break;
  185. case 'memcache':
  186. $memcache = new \Memcache();
  187. $memcache->connect($this->config->get('system.cache.memcache.server', 'localhost'),
  188. $this->config->get('system.cache.memcache.port', 11211));
  189. $driver = new DoctrineCache\MemcacheCache();
  190. $driver->setMemcache($memcache);
  191. break;
  192. case 'memcached':
  193. $memcached = new \Memcached();
  194. $memcached->addServer($this->config->get('system.cache.memcached.server', 'localhost'),
  195. $this->config->get('system.cache.memcached.port', 11211));
  196. $driver = new DoctrineCache\MemcachedCache();
  197. $driver->setMemcached($memcached);
  198. break;
  199. case 'redis':
  200. $redis = new \Redis();
  201. $socket = $this->config->get('system.cache.redis.socket', false);
  202. $password = $this->config->get('system.cache.redis.password', false);
  203. if ($socket) {
  204. $redis->connect($socket);
  205. } else {
  206. $redis->connect($this->config->get('system.cache.redis.server', 'localhost'),
  207. $this->config->get('system.cache.redis.port', 6379));
  208. }
  209. // Authenticate with password if set
  210. if ($password && !$redis->auth($password)) {
  211. throw new \RedisException('Redis authentication failed');
  212. }
  213. $driver = new DoctrineCache\RedisCache();
  214. $driver->setRedis($redis);
  215. break;
  216. default:
  217. $driver = new DoctrineCache\FilesystemCache($this->cache_dir);
  218. break;
  219. }
  220. return $driver;
  221. }
  222. /**
  223. * Gets a cached entry if it exists based on an id. If it does not exist, it returns false
  224. *
  225. * @param string $id the id of the cached entry
  226. *
  227. * @return object|bool returns the cached entry, can be any type, or false if doesn't exist
  228. */
  229. public function fetch($id)
  230. {
  231. if ($this->enabled) {
  232. return $this->driver->fetch($id);
  233. } else {
  234. return false;
  235. }
  236. }
  237. /**
  238. * Stores a new cached entry.
  239. *
  240. * @param string $id the id of the cached entry
  241. * @param array|object $data the data for the cached entry to store
  242. * @param int $lifetime the lifetime to store the entry in seconds
  243. */
  244. public function save($id, $data, $lifetime = null)
  245. {
  246. if ($this->enabled) {
  247. if ($lifetime === null) {
  248. $lifetime = $this->getLifetime();
  249. }
  250. $this->driver->save($id, $data, $lifetime);
  251. }
  252. }
  253. /**
  254. * Deletes an item in the cache based on the id
  255. *
  256. * @param string $id the id of the cached data entry
  257. * @return bool true if the item was deleted successfully
  258. */
  259. public function delete($id)
  260. {
  261. if ($this->enabled) {
  262. return $this->driver->delete($id);
  263. }
  264. return false;
  265. }
  266. /**
  267. * Returns a boolean state of whether or not the item exists in the cache based on id key
  268. *
  269. * @param string $id the id of the cached data entry
  270. * @return bool true if the cached items exists
  271. */
  272. public function contains($id)
  273. {
  274. if ($this->enabled) {
  275. return $this->driver->contains(($id));
  276. }
  277. return false;
  278. }
  279. /**
  280. * Getter method to get the cache key
  281. */
  282. public function getKey()
  283. {
  284. return $this->key;
  285. }
  286. /**
  287. * Setter method to set key (Advanced)
  288. */
  289. public function setKey($key)
  290. {
  291. $this->key = $key;
  292. $this->driver->setNamespace($this->key);
  293. }
  294. /**
  295. * Helper method to clear all Grav caches
  296. *
  297. * @param string $remove standard|all|assets-only|images-only|cache-only
  298. *
  299. * @return array
  300. */
  301. public static function clearCache($remove = 'standard')
  302. {
  303. $locator = Grav::instance()['locator'];
  304. $output = [];
  305. $user_config = USER_DIR . 'config/system.yaml';
  306. switch ($remove) {
  307. case 'all':
  308. $remove_paths = self::$all_remove;
  309. break;
  310. case 'assets-only':
  311. $remove_paths = self::$assets_remove;
  312. break;
  313. case 'images-only':
  314. $remove_paths = self::$images_remove;
  315. break;
  316. case 'cache-only':
  317. $remove_paths = self::$cache_remove;
  318. break;
  319. case 'tmp-only':
  320. $remove_paths = self::$tmp_remove;
  321. break;
  322. default:
  323. if (Grav::instance()['config']->get('system.cache.clear_images_by_default')) {
  324. $remove_paths = self::$standard_remove;
  325. } else {
  326. $remove_paths = self::$standard_remove_no_images;
  327. }
  328. }
  329. // Clearing cache event to add paths to clear
  330. Grav::instance()->fireEvent('onBeforeCacheClear', new Event(['remove' => $remove, 'paths' => &$remove_paths]));
  331. foreach ($remove_paths as $stream) {
  332. // Convert stream to a real path
  333. try {
  334. $path = $locator->findResource($stream, true, true);
  335. if($path === false) continue;
  336. $anything = false;
  337. $files = glob($path . '/*');
  338. if (is_array($files)) {
  339. foreach ($files as $file) {
  340. if (is_link($file)) {
  341. $output[] = '<yellow>Skipping symlink: </yellow>' . $file;
  342. } elseif (is_file($file)) {
  343. if (@unlink($file)) {
  344. $anything = true;
  345. }
  346. } elseif (is_dir($file)) {
  347. if (Folder::delete($file)) {
  348. $anything = true;
  349. }
  350. }
  351. }
  352. }
  353. if ($anything) {
  354. $output[] = '<red>Cleared: </red>' . $path . '/*';
  355. }
  356. } catch (\Exception $e) {
  357. // stream not found or another error while deleting files.
  358. $output[] = '<red>ERROR: </red>' . $e->getMessage();
  359. }
  360. }
  361. $output[] = '';
  362. if (($remove == 'all' || $remove == 'standard') && file_exists($user_config)) {
  363. touch($user_config);
  364. $output[] = '<red>Touched: </red>' . $user_config;
  365. $output[] = '';
  366. }
  367. // Clear stat cache
  368. @clearstatcache();
  369. // Clear opcache
  370. if (function_exists('opcache_reset')) {
  371. @opcache_reset();
  372. }
  373. return $output;
  374. }
  375. /**
  376. * Set the cache lifetime programmatically
  377. *
  378. * @param int $future timestamp
  379. */
  380. public function setLifetime($future)
  381. {
  382. if (!$future) {
  383. return;
  384. }
  385. $interval = $future - $this->now;
  386. if ($interval > 0 && $interval < $this->getLifetime()) {
  387. $this->lifetime = $interval;
  388. }
  389. }
  390. /**
  391. * Retrieve the cache lifetime (in seconds)
  392. *
  393. * @return mixed
  394. */
  395. public function getLifetime()
  396. {
  397. if ($this->lifetime === null) {
  398. $this->lifetime = $this->config->get('system.cache.lifetime') ?: 604800; // 1 week default
  399. }
  400. return $this->lifetime;
  401. }
  402. /**
  403. * Returns the current driver name
  404. *
  405. * @return mixed
  406. */
  407. public function getDriverName()
  408. {
  409. return $this->driver_name;
  410. }
  411. /**
  412. * Returns the current driver setting
  413. *
  414. * @return mixed
  415. */
  416. public function getDriverSetting()
  417. {
  418. return $this->driver_setting;
  419. }
  420. /**
  421. * is this driver a volatile driver in that it resides in PHP process memory
  422. *
  423. * @param $setting
  424. * @return bool
  425. */
  426. public function isVolatileDriver($setting)
  427. {
  428. if (in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'])) {
  429. return true;
  430. } else {
  431. return false;
  432. }
  433. }
  434. }