Cache.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. <?php
  2. namespace Gregwar\Cache;
  3. /**
  4. * A cache system based on files
  5. *
  6. * @author Gregwar <g.passault@gmail.com>
  7. */
  8. class Cache implements CacheInterface
  9. {
  10. /**
  11. * Cache directory
  12. */
  13. protected $cacheDirectory;
  14. /**
  15. * Use a different directory as actual cache
  16. * @var string
  17. */
  18. protected $actualCacheDirectory = null;
  19. /**
  20. * Prefix directories size
  21. *
  22. * For instance, if the file is helloworld.txt and the prefix size is
  23. * 5, the cache file will be: h/e/l/l/o/helloworld.txt
  24. *
  25. * This is useful to avoid reaching a too large number of files into the
  26. * cache system directories
  27. * @var int
  28. */
  29. protected $prefixSize = 5;
  30. /**
  31. * Directory mode
  32. *
  33. * Allows setting of the access mode for the directories created.
  34. * @var int
  35. */
  36. protected $directoryMode = 0755;
  37. /**
  38. * Constructs the cache system
  39. *
  40. * @param string $cacheDirectory the cache directory
  41. */
  42. public function __construct($cacheDirectory = 'cache')
  43. {
  44. $this->cacheDirectory = $cacheDirectory;
  45. }
  46. /**
  47. * Sets the cache directory
  48. *
  49. * @param string $cacheDirectory the cache directory
  50. * @return self
  51. */
  52. public function setCacheDirectory($cacheDirectory)
  53. {
  54. $this->cacheDirectory = $cacheDirectory;
  55. return $this;
  56. }
  57. /**
  58. * Gets the cache directory
  59. *
  60. * @return string the cache directory
  61. */
  62. public function getCacheDirectory()
  63. {
  64. return $this->cacheDirectory;
  65. }
  66. /**
  67. * Sets the actual cache directory
  68. *
  69. * @param string $actualCacheDirectory the actual cache directory
  70. * @return self
  71. */
  72. public function setActualCacheDirectory($actualCacheDirectory = null)
  73. {
  74. $this->actualCacheDirectory = $actualCacheDirectory;
  75. return $this;
  76. }
  77. /**
  78. * Returns the actual cache directory
  79. */
  80. public function getActualCacheDirectory()
  81. {
  82. return $this->actualCacheDirectory ?: $this->cacheDirectory;
  83. }
  84. /**
  85. * Change the prefix size
  86. *
  87. * @param int $prefixSize the size of the prefix directories
  88. * @return self
  89. */
  90. public function setPrefixSize($prefixSize)
  91. {
  92. $this->prefixSize = $prefixSize;
  93. return $this;
  94. }
  95. /**
  96. * Change the directory mode
  97. *
  98. * @param int $directoryMode the directory mode to use
  99. * @return self
  100. */
  101. public function setDirectoryMode($directoryMode)
  102. {
  103. if (!$directoryMode) {
  104. $directoryMode = 0755;
  105. }
  106. $this->directoryMode = $directoryMode;
  107. return $this;
  108. }
  109. /**
  110. * Creates a directory
  111. *
  112. * @param string $directory the target directory
  113. */
  114. protected function mkdir($directory)
  115. {
  116. if (!is_dir($directory)) {
  117. @mkdir($directory, $this->directoryMode, true);
  118. }
  119. }
  120. /**
  121. * Gets the cache file name
  122. *
  123. * @param string $filename the name of the cache file
  124. * @param bool $actual get the actual file or the public file
  125. * @param bool $mkdir a boolean to enable/disable the construction of the
  126. * cache file directory
  127. * @return string
  128. */
  129. public function getCacheFile($filename, $actual = false, $mkdir = false)
  130. {
  131. $path = array();
  132. // Getting the length of the filename before the extension
  133. $parts = explode('.', $filename);
  134. $len = strlen($parts[0]);
  135. for ($i=0; $i<min($len, $this->prefixSize); $i++) {
  136. $path[] = $filename[$i];
  137. }
  138. $path = implode('/', $path);
  139. if ($mkdir) {
  140. $actualDir = $this->getActualCacheDirectory() . '/' . $path;
  141. $this->mkdir($actualDir);
  142. }
  143. $path .= '/' . $filename;
  144. if ($actual) {
  145. return $this->getActualCacheDirectory() . '/' . $path;
  146. } else {
  147. return $this->getCacheDirectory() . '/' . $path;
  148. }
  149. }
  150. /**
  151. * Checks that the cache conditions are respected
  152. *
  153. * @param string $cacheFile the cache file
  154. * @param array $conditions an array of conditions to check
  155. * @return bool
  156. * @throws \Exception
  157. */
  158. protected function checkConditions($cacheFile, array $conditions = array())
  159. {
  160. // Implicit condition: the cache file should exist
  161. if (!file_exists($cacheFile)) {
  162. return false;
  163. }
  164. foreach ($conditions as $type => $value) {
  165. switch ($type) {
  166. case 'maxage':
  167. case 'max-age':
  168. // Return false if the file is older than $value
  169. $age = time() - filemtime($cacheFile);
  170. if ($age > $value) {
  171. return false;
  172. }
  173. break;
  174. case 'younger-than':
  175. case 'youngerthan':
  176. // Return false if the file is older than the file $value, or the files $value
  177. $check = function($filename) use ($cacheFile) {
  178. return !file_exists($filename) || filemtime($cacheFile) < filemtime($filename);
  179. };
  180. if (!is_array($value)) {
  181. if (!$this->isRemote($value) && $check($value)) {
  182. return false;
  183. }
  184. } else {
  185. foreach ($value as $file) {
  186. if (!$this->isRemote($file) && $check($file)) {
  187. return false;
  188. }
  189. }
  190. }
  191. break;
  192. default:
  193. throw new \Exception('Cache condition '.$type.' not supported');
  194. }
  195. }
  196. return true;
  197. }
  198. /**
  199. * Checks if the target filename exists in the cache and if the conditions
  200. * are respected
  201. *
  202. * @param string $filename the filename
  203. * @param array $conditions the conditions to respect
  204. * @return bool
  205. */
  206. public function exists($filename, array $conditions = array())
  207. {
  208. $cacheFile = $this->getCacheFile($filename, true);
  209. return $this->checkConditions($cacheFile, $conditions);
  210. }
  211. /**
  212. * Alias for exists
  213. *
  214. * @param string $filename the filename
  215. * @param array $conditions the conditions to respect
  216. * @return bool
  217. */
  218. public function check($filename, array $conditions = array())
  219. {
  220. return $this->exists($filename, $conditions);
  221. }
  222. /**
  223. * Write data in the cache
  224. *
  225. * @param string $filename the name of the cache file
  226. * @param string $contents the contents to store
  227. * @return self
  228. */
  229. public function set($filename, $contents = '')
  230. {
  231. $cacheFile = $this->getCacheFile($filename, true, true);
  232. file_put_contents($cacheFile, $contents, \LOCK_EX);
  233. return $this;
  234. }
  235. /**
  236. * Alias for set()
  237. *
  238. * @param string $filename the name of the cache file
  239. * @param string $contents the contents to store
  240. * @return self
  241. */
  242. public function write($filename, $contents = '')
  243. {
  244. return $this->set($filename, $contents);
  245. }
  246. /**
  247. * Get data from the cache
  248. *
  249. * @param string $filename the cache file name
  250. * @param array $conditions
  251. * @return null|string
  252. */
  253. public function get($filename, array $conditions = array())
  254. {
  255. if ($this->exists($filename, $conditions)) {
  256. return file_get_contents($this->getCacheFile($filename, true));
  257. } else {
  258. return null;
  259. }
  260. }
  261. /**
  262. * Is this URL remote?
  263. *
  264. * @param string $file
  265. * @return bool
  266. */
  267. protected function isRemote($file)
  268. {
  269. if (preg_match('/^([a-z]+):\/\//', $file, $match)) {
  270. return ($match[1] != 'file');
  271. }
  272. return false;
  273. }
  274. /**
  275. * Get or create the cache entry
  276. *
  277. * @param string $filename the cache file name
  278. * @param array $conditions an array of conditions about expiration
  279. * @param \Closure $function the closure to call if the file does not exist
  280. * @param bool $file returns the cache file or the file contents
  281. * @param bool $actual returns the actual cache file
  282. * @return string
  283. * @throws \InvalidArgumentException
  284. */
  285. public function getOrCreate($filename, array $conditions = array(), $function, $file = false, $actual = false)
  286. {
  287. if (!is_callable($function)) {
  288. throw new \InvalidArgumentException('The argument $function should be callable');
  289. }
  290. $cacheFile = $this->getCacheFile($filename, true, true);
  291. $data = null;
  292. if (!$this->check($filename, $conditions)) {
  293. if(file_exists($cacheFile)) {
  294. unlink($cacheFile);
  295. }
  296. $data = call_user_func($function, $cacheFile);
  297. // Test if the closure wrote the file or if it returned the data
  298. if (!file_exists($cacheFile)) {
  299. $this->set($filename, $data);
  300. } else {
  301. $data = file_get_contents($cacheFile);
  302. }
  303. }
  304. return $file ? $this->getCacheFile($filename, $actual) : file_get_contents($cacheFile);
  305. }
  306. /**
  307. * Alias to getOrCreate with $file = true
  308. *
  309. * @param string $filename the cache file name
  310. * @param array $conditions an array of conditions about expiration
  311. * @param \Closure $function the closure to call if the file does not exist
  312. * @param bool $actual returns the actual cache file
  313. * @return string
  314. * @throws \InvalidArgumentException
  315. */
  316. public function getOrCreateFile($filename, array $conditions = array(), $function, $actual = false)
  317. {
  318. return $this->getOrCreate($filename, $conditions, $function, true, $actual);
  319. }
  320. }