1 <?php 2 /* Copyright (c) 2012, Geert Bergman (geert@scrivo.nl) 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, 9 * this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright notice, 11 * this list of conditions and the following disclaimer in the documentation 12 * and/or other materials provided with the distribution. 13 * 3. Neither the name of "Scrivo" nor the names of its contributors may be 14 * used to endorse or promote products derived from this software without 15 * specific prior written permission. 16 * 17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 * POSSIBILITY OF SUCH DAMAGE. 28 * 29 * $Id: FileCache.php 841 2013-08-19 22:19:47Z geert $ 30 */ 31 32 /** 33 * Implementation of the \Scrivo\Cache\FileCache class. 34 */ 35 36 namespace Scrivo\Cache; 37 38 /** 39 * Implementation of a file cache. 40 * 41 * This is an implmentation of a file cache in PHP. It allows you to store 42 * serializable objects on disk. It was designed to be a fallback for the 43 * APC user cache, but nonetheless a very reasonable alternative. 44 * 45 * Some configuration notes. If possible use a RAM disk, apart from the speed 46 * benefits it probably is also easier to limit the amount of space used by 47 * the cache when using a RAM disk, else you'll have to resort to disk quotas. 48 * 49 * There is no software limit on the amount of data the cache uses. A garbage 50 * collector is run each FileCache::$gcInterval store requests. If data 51 * cannot be written the cache will acively purge data, keeping a certain 52 * percentage of the most frequently used entries in the cache. 53 * 54 * Garbage collection only removes expired entries. When purging data another 55 * alogrithm is used: Data that was stored but never used will be deleted 56 * inmediately, the rest is sorted based on access time and then a given 57 * percentage (FileCache::$pctToKeepAfterPurge) of the last accessed files 58 * will be saved, the rest is removed. 59 * 60 * Note: one of the former versions supported file locking with flock. But 61 * that does not work in a threaded web server and ISAPI. Since that is the 62 * common case nowadays file locking is dropped. Is it thread save? I believe 63 * so, but that is only because unserialize will return null when reading 64 * corrupted (not fully written) entries. Furthermore beacuse of PHP's 65 * problems with file locking cache slams are not allowed. So only the first 66 * thread is allowed to write a file, later threads are not. 67 */ 68 class FileCache implements \Scrivo\Cache { 69 70 /** 71 * The location of the cache directory. 72 * 73 * @var \Scrivo\String 74 */ 75 private $dir; 76 77 /** 78 * The gargbage collection interval. This interval is measured in 79 * number of store requests. 80 * 81 * @var int 82 */ 83 private $gcInterval; 84 85 /** 86 * The counter for the number of store requests. 87 * 88 * @var int 89 */ 90 private $storeCount = 0; 91 92 /** 93 * Percentage of most frequently accessed file that will be kept after 94 * the cache is purged when there's not enough space left. 95 * 96 * @var int 97 */ 98 private $pctToKeepAfterPurge; 99 100 /** 101 * List of troublesome characters in file names. 102 * 103 * @var \Scrivo\String[] 104 */ 105 private $reservedCharacters; 106 107 /** 108 * List of character sequences to escape troublesome characters in file 109 * names. 110 * 111 * @var \Scrivo\String[] 112 */ 113 private $escapedCharacters; 114 115 /** 116 * Create a file cache. 117 * 118 * @param \Scrivo\String $dir The location where the cache schould 119 * store the files. The default is the 'ScrivoCache' folder in the 120 * system's temp directory. 121 * @param int $gcInterval The interval at which to run the garbage 122 * collector measured in store requests. 123 * @param int $pctToKeepAfterPurge Percentage of items that were accessed 124 * at least once that you want to keep after a cache purge due to 125 * storage shortage. 126 */ 127 public function __construct(\Scrivo\String $dir=null, $gcInterval=50, 128 $pctToKeepAfterPurge=50) { 129 $this->gcInterval = $gcInterval; 130 $this->pctToKeepAfterPurge = $pctToKeepAfterPurge; 131 $this->reservedCharacters = \Scrivo\String::create(array( 132 "@","/","\\","?","%","*",":","|","\"","<",">",".")); 133 $this->escapedCharacters = \Scrivo\String::create(array( 134 "@0","@1","@2","@3","@4","@5","@6","@7","@8","@9","@A","@B")); 135 if (!$dir) { 136 $dir = sys_get_temp_dir()."/ScrivoCache"; 137 } 138 if (!file_exists($dir)) { 139 mkdir($dir); 140 } 141 $this->dir = new \Scrivo\String($dir."/"); 142 } 143 144 /** 145 * Just a wrapper for fopen that throws an exception instead of an error. 146 * 147 * @param string $file 148 */ 149 150 private function fopen($file) { 151 if (!$fp = fopen($file, "w")) { 152 throw new \Scrivo\SystemException( 153 "Could not create cache file '$file'"); 154 } 155 return $fp; 156 } 157 158 /** 159 * Just a wrapper for touch that throws an exception instead of returning 160 * a status. 161 * 162 * @param string $file 163 * @param string $ttl 164 */ 165 private function touch($file, $ttl) { 166 if (!touch($file, time()+$ttl, time()-2)) { 167 throw new \Scrivo\SystemException( 168 "Could not touch cache file '$file'"); 169 } 170 } 171 172 /** 173 * Just a wrapper for unlink that throws an exception instead of returning 174 * a status. 175 * 176 * @param string $file 177 */ 178 private function unlink($file) { 179 if (!unlink($file)) { 180 throw new \Scrivo\SystemException( 181 "Could not delete cache file '$file'"); 182 } 183 } 184 185 /** 186 * Create a file name from a key, taking in account problematic characters 187 * for a file name. 188 * 189 * @param \Scrivo\String $key Key name to create a file name for. 190 */ 191 private function getFile(\Scrivo\String $key) { 192 return $this->dir . $key->replace($this->reservedCharacters, 193 $this->escapedCharacters); 194 } 195 196 /** 197 * Clear the cache from its most irrelevant items. 198 * 199 * You can use this function to free up a certain amount of the file cache 200 * First it deletes the items that were stored but never accessed. The 201 * items that were accessed at least once are sorted on access time and 202 * prec_to_keep of the most recently accessed files will be kept in the 203 * cache. The others will be deleted. 204 * 205 * @param int $percentageToKeep Percentage of items that were accessed 206 * at least once that you want to keep. 207 */ 208 public function purge($percentageToKeep=0) { 209 clearstatcache(); 210 $m=0; 211 $ref = array(); 212 if ($dh = opendir($this->dir)) { 213 while (($file = readdir($dh)) !== false) { 214 $s = stat($this->dir.$file); 215 if (!($s["mode"] & 040000)) { 216 if ($s["atime"] < $s["ctime"]) { 217 // remove not yet accessed items 218 $this->unlink($this->dir.$file); 219 $m++; 220 } else { 221 $ref[$file] = $s["atime"]; 222 } 223 } 224 } 225 closedir($dh); 226 // remove files that were accessed last 227 arsort($ref); 228 $n=0; 229 $i = intval(count($ref)*$percentageToKeep/100); 230 $rem = array_slice($ref, 0, $i); 231 foreach ($rem as $k=>$d) { 232 $this->unlink($this->dir.$k); 233 $n++; 234 } 235 } 236 //error_log("Cache purged: $m items that were never accessed and " . 237 // "$n of the most infrequently used items were removed."); 238 } 239 240 /** 241 * Run the garbage collector: delete all expired cache entries. 242 */ 243 private function gc() { 244 clearstatcache(); 245 $i=0; 246 if ($dh = opendir($this->dir)) { 247 while (($file = readdir($dh)) !== false) { 248 $s = stat($this->dir.$file); 249 if (!($s["mode"] & 040000)) { 250 if ($s["mtime"] < time()) { 251 $this->unlink($this->dir.$file); 252 $i++; 253 } 254 } 255 } 256 closedir($dh); 257 } 258 //error_log("File cache garbage collected: $i items removed"); 259 } 260 261 /** 262 * Store a variable in the cache. 263 * 264 * Store any serializable variable in the cache. Note that it is not 265 * possible to overwrite an existing entry (cache slam). Such an event 266 * will not raise an error but the function will report it. 267 * 268 * @param \Scrivo\String $key A cache unique name for the key. 269 * @param mixed $val The (serializable) variabele to strore. 270 * @param int $ttl Time to live in seconds. 271 * 272 * @return int DATA_STORED if the variable was succesfully stored, 273 * CACHE_SLAM if key already exists or NO_SPACE if there is not 274 * enough space left to store the value. 275 * 276 * @throws \Scrivo\SystemException When trying to store a NULL value or 277 * when a file operation fails. 278 */ 279 public function store(\Scrivo\String $key, $val, $ttl=3600) { 280 if ($val === null) { 281 throw new \Scrivo\SystemException( 282 "Can't store null values in the cache"); 283 } 284 // The name for the cache file. 285 $file = $this->getFile($key); 286 // If the file already exists, and other thread is already writing 287 // the file and we're done here. 288 if (file_exists($file)) { 289 return self::CACHE_SLAM; 290 } 291 // Run the garbage collector at the specified probability. 292 if (($this->storeCount % $this->gcInterval) == $this->gcInterval - 1) { 293 $this->gc(); 294 } 295 // Get the data to store. 296 $data = new \Scrivo\ByteArray(serialize($val)); 297 // Create the file and bail out not succesfull. 298 $fp = $this->fopen($file); 299 // Write the data. 300 if (fwrite($fp, (string)$data) != $data->length) { 301 // Data was not fully written: drop infreq. used and unused entries. 302 fclose($fp); 303 $this->delete($key); 304 $this->purge($this->pctToKeepAfterPurge); 305 // try again 306 $fp = $this->fopen($file); 307 if (fwrite($fp, (string)$data) != $data->length) { 308 // Data was not fully written again: close and remove the file. 309 fclose($fp); 310 $this->delete($key); 311 return self::NO_SPACE; 312 } 313 } 314 // Now we have a file touch it. 315 fclose($fp); 316 $this->touch($file, $ttl); 317 318 $this->storeCount++; 319 return self::DATA_STORED; 320 } 321 322 /** 323 * Store a variable in the cache, overwrite it if it already exists. 324 * 325 * Store any serializable variable in the cache. It is guaranteed that 326 * the data will be written. But note that it is not guaranteed that the 327 * next fetch will retrieve this value. 328 * 329 * @param \Scrivo\String $key A cache unique name for the key. 330 * @param mixed $val The (serializable) variabele to strore. 331 * @param int $ttl Time to live in seconds. 332 * 333 * @return int DATA_STORED if the variable was succesfully stored or 334 * NO_SPACE if there is not enough space left to store the value. 335 * 336 * @throws \Scrivo\SystemException When trying to store a NULL value or 337 * when a file operation fails. 338 */ 339 public function overwrite(\Scrivo\String $key, $val, $ttl=3600) { 340 // Theoretically just after delete an other thread can store a value. 341 for ($tmp=self::CACHE_SLAM; $tmp==self::CACHE_SLAM;) { 342 $this->delete($key); 343 $tmp = $this->store($key, $val, $ttl); 344 } 345 return $tmp; 346 } 347 348 /** 349 * Delete/remove a cache entry. 350 * 351 * @param \Scrivo\String $key A cache unique name for the key. 352 */ 353 public function delete(\Scrivo\String $key) { 354 $file = $this->getFile($key); 355 if (file_exists($file)) { 356 $this->unlink($file); 357 } 358 } 359 360 /** 361 * Retrieve a value from the cache. 362 * 363 * @param \Scrivo\String $key The key for which to retrieve the value. 364 * 365 * @return mixed The value of the stored variable or NULL if the key 366 * does not exists or is expired. 367 */ 368 public function fetch(\Scrivo\String $key) { 369 $file = $this->getFile($key); 370 $res = null; 371 // Suppressing the message is probably faster then calling file_exists 372 // and stat. 373 if ($s = @stat($file)) { 374 if ($s["mtime"] < time()) { 375 $this->delete($key); 376 } else { 377 $tmp = file_get_contents($file); 378 // if (strtoupper(substr(PHP_OS,0,3))==='WIN') 379 // touch($file,date('U',filemtime($file)),time()); 380 // 381 if ($tmp) { 382 $res = unserialize($tmp); 383 } 384 } 385 } 386 return $res ? $res : null; 387 } 388 389 /** 390 * List all entries in the cache. 391 * 392 * The cache list is an array in which the cache keys are the keys of 393 * the array entries and the data of the entries are objects ot type 394 * stdClass that contain at least contain the following properties: 395 * 396 * * accessed: the access time 397 * * expires: the expiration time 398 * * created: the creation time 399 * * size: the size of the entry 400 * 401 * @return object[] A array of objects that describe the current cache 402 * entries. 403 */ 404 public function entryList() { 405 $l = array(); 406 if ($dh = opendir($this->dir)) { 407 while (($file = readdir($dh)) !== false) { 408 $s = stat($this->dir.$file); 409 if (!($s['mode'] & 040000)) { 410 $k = \Scrivo\String::create($file)->replace( 411 $this->escapedCharacters, $this->reservedCharacters); 412 $l[(string)$k] = (object)array( 413 "accessed" => $s["atime"], 414 "expires" => $s["mtime"], 415 "created" => $s["ctime"], 416 "size" => $s["size"] 417 ); 418 } 419 } 420 closedir($dh); 421 } 422 return $l; 423 } 424 425 } 426 427 ?>
Documentation generated by phpDocumentor 2.0.0a12 and ScrivoDocumentor on August 29, 2013