Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
100.00% |
1 / 1 |
|
100.00% |
12 / 12 |
CRAP | |
100.00% |
133 / 133 |
FileCache | |
100.00% |
1 / 1 |
|
100.00% |
12 / 12 |
40 | |
100.00% |
133 / 133 |
__construct(\Scrivo\String $dir=null, $gcInterval=50, $pctToKeepAfterPurge=50) | |
100.00% |
1 / 1 |
3 | |
100.00% |
14 / 14 |
|||
fopen($file) | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
touch($file, $ttl) | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
unlink($file) | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
getFile(\Scrivo\String $key) | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
purge($percentageToKeep=0) | |
100.00% |
1 / 1 |
6 | |
100.00% |
25 / 25 |
|||
gc() | |
100.00% |
1 / 1 |
5 | |
100.00% |
15 / 15 |
|||
store(\Scrivo\String $key, $val, $ttl=3600) | |
100.00% |
1 / 1 |
6 | |
100.00% |
25 / 25 |
|||
overwrite(\Scrivo\String $key, $val, $ttl=3600) | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
delete(\Scrivo\String $key) | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
fetch(\Scrivo\String $key) | |
100.00% |
1 / 1 |
5 | |
100.00% |
12 / 12 |
|||
entryList() | |
100.00% |
1 / 1 |
4 | |
100.00% |
18 / 18 |
<?php | |
/* Copyright (c) 2012, Geert Bergman (geert@scrivo.nl) | |
* All rights reserved. | |
* | |
* Redistribution and use in source and binary forms, with or without | |
* modification, are permitted provided that the following conditions are met: | |
* | |
* 1. Redistributions of source code must retain the above copyright notice, | |
* this list of conditions and the following disclaimer. | |
* 2. Redistributions in binary form must reproduce the above copyright notice, | |
* this list of conditions and the following disclaimer in the documentation | |
* and/or other materials provided with the distribution. | |
* 3. Neither the name of "Scrivo" nor the names of its contributors may be | |
* used to endorse or promote products derived from this software without | |
* specific prior written permission. | |
* | |
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | |
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
* POSSIBILITY OF SUCH DAMAGE. | |
* | |
* $Id: FileCache.php 841 2013-08-19 22:19:47Z geert $ | |
*/ | |
/** | |
* Implementation of the \Scrivo\Cache\FileCache class. | |
*/ | |
namespace Scrivo\Cache; | |
/** | |
* Implementation of a file cache. | |
* | |
* This is an implmentation of a file cache in PHP. It allows you to store | |
* serializable objects on disk. It was designed to be a fallback for the | |
* APC user cache, but nonetheless a very reasonable alternative. | |
* | |
* Some configuration notes. If possible use a RAM disk, apart from the speed | |
* benefits it probably is also easier to limit the amount of space used by | |
* the cache when using a RAM disk, else you'll have to resort to disk quotas. | |
* | |
* There is no software limit on the amount of data the cache uses. A garbage | |
* collector is run each FileCache::$gcInterval store requests. If data | |
* cannot be written the cache will acively purge data, keeping a certain | |
* percentage of the most frequently used entries in the cache. | |
* | |
* Garbage collection only removes expired entries. When purging data another | |
* alogrithm is used: Data that was stored but never used will be deleted | |
* inmediately, the rest is sorted based on access time and then a given | |
* percentage (FileCache::$pctToKeepAfterPurge) of the last accessed files | |
* will be saved, the rest is removed. | |
* | |
* Note: one of the former versions supported file locking with flock. But | |
* that does not work in a threaded web server and ISAPI. Since that is the | |
* common case nowadays file locking is dropped. Is it thread save? I believe | |
* so, but that is only because unserialize will return null when reading | |
* corrupted (not fully written) entries. Furthermore beacuse of PHP's | |
* problems with file locking cache slams are not allowed. So only the first | |
* thread is allowed to write a file, later threads are not. | |
*/ | |
class FileCache implements \Scrivo\Cache { | |
/** | |
* The location of the cache directory. | |
* | |
* @var \Scrivo\String | |
*/ | |
private $dir; | |
/** | |
* The gargbage collection interval. This interval is measured in | |
* number of store requests. | |
* | |
* @var int | |
*/ | |
private $gcInterval; | |
/** | |
* The counter for the number of store requests. | |
* | |
* @var int | |
*/ | |
private $storeCount = 0; | |
/** | |
* Percentage of most frequently accessed file that will be kept after | |
* the cache is purged when there's not enough space left. | |
* | |
* @var int | |
*/ | |
private $pctToKeepAfterPurge; | |
/** | |
* List of troublesome characters in file names. | |
* | |
* @var \Scrivo\String[] | |
*/ | |
private $reservedCharacters; | |
/** | |
* List of character sequences to escape troublesome characters in file | |
* names. | |
* | |
* @var \Scrivo\String[] | |
*/ | |
private $escapedCharacters; | |
/** | |
* Create a file cache. | |
* | |
* @param \Scrivo\String $dir The location where the cache schould | |
* store the files. The default is the 'ScrivoCache' folder in the | |
* system's temp directory. | |
* @param int $gcInterval The interval at which to run the garbage | |
* collector measured in store requests. | |
* @param int $pctToKeepAfterPurge Percentage of items that were accessed | |
* at least once that you want to keep after a cache purge due to | |
* storage shortage. | |
*/ | |
public function __construct(\Scrivo\String $dir=null, $gcInterval=50, | |
$pctToKeepAfterPurge=50) { | |
$this->gcInterval = $gcInterval; | |
$this->pctToKeepAfterPurge = $pctToKeepAfterPurge; | |
$this->reservedCharacters = \Scrivo\String::create(array( | |
"@","/","\\","?","%","*",":","|","\"","<",">",".")); | |
$this->escapedCharacters = \Scrivo\String::create(array( | |
"@0","@1","@2","@3","@4","@5","@6","@7","@8","@9","@A","@B")); | |
if (!$dir) { | |
$dir = sys_get_temp_dir()."/ScrivoCache"; | |
} | |
if (!file_exists($dir)) { | |
mkdir($dir); | |
} | |
$this->dir = new \Scrivo\String($dir."/"); | |
} | |
/** | |
* Just a wrapper for fopen that throws an exception instead of an error. | |
* | |
* @param string $file | |
*/ | |
private function fopen($file) { | |
if (!$fp = fopen($file, "w")) { | |
throw new \Scrivo\SystemException( | |
"Could not create cache file '$file'"); | |
} | |
return $fp; | |
} | |
/** | |
* Just a wrapper for touch that throws an exception instead of returning | |
* a status. | |
* | |
* @param string $file | |
* @param string $ttl | |
*/ | |
private function touch($file, $ttl) { | |
if (!touch($file, time()+$ttl, time()-2)) { | |
throw new \Scrivo\SystemException( | |
"Could not touch cache file '$file'"); | |
} | |
} | |
/** | |
* Just a wrapper for unlink that throws an exception instead of returning | |
* a status. | |
* | |
* @param string $file | |
*/ | |
private function unlink($file) { | |
if (!unlink($file)) { | |
throw new \Scrivo\SystemException( | |
"Could not delete cache file '$file'"); | |
} | |
} | |
/** | |
* Create a file name from a key, taking in account problematic characters | |
* for a file name. | |
* | |
* @param \Scrivo\String $key Key name to create a file name for. | |
*/ | |
private function getFile(\Scrivo\String $key) { | |
return $this->dir . $key->replace($this->reservedCharacters, | |
$this->escapedCharacters); | |
} | |
/** | |
* Clear the cache from its most irrelevant items. | |
* | |
* You can use this function to free up a certain amount of the file cache | |
* First it deletes the items that were stored but never accessed. The | |
* items that were accessed at least once are sorted on access time and | |
* prec_to_keep of the most recently accessed files will be kept in the | |
* cache. The others will be deleted. | |
* | |
* @param int $percentageToKeep Percentage of items that were accessed | |
* at least once that you want to keep. | |
*/ | |
public function purge($percentageToKeep=0) { | |
clearstatcache(); | |
$m=0; | |
$ref = array(); | |
if ($dh = opendir($this->dir)) { | |
while (($file = readdir($dh)) !== false) { | |
$s = stat($this->dir.$file); | |
if (!($s["mode"] & 040000)) { | |
if ($s["atime"] < $s["ctime"]) { | |
// remove not yet accessed items | |
$this->unlink($this->dir.$file); | |
$m++; | |
} else { | |
$ref[$file] = $s["atime"]; | |
} | |
} | |
} | |
closedir($dh); | |
// remove files that were accessed last | |
arsort($ref); | |
$n=0; | |
$i = intval(count($ref)*$percentageToKeep/100); | |
$rem = array_slice($ref, 0, $i); | |
foreach ($rem as $k=>$d) { | |
$this->unlink($this->dir.$k); | |
$n++; | |
} | |
} | |
//error_log("Cache purged: $m items that were never accessed and " . | |
// "$n of the most infrequently used items were removed."); | |
} | |
/** | |
* Run the garbage collector: delete all expired cache entries. | |
*/ | |
private function gc() { | |
clearstatcache(); | |
$i=0; | |
if ($dh = opendir($this->dir)) { | |
while (($file = readdir($dh)) !== false) { | |
$s = stat($this->dir.$file); | |
if (!($s["mode"] & 040000)) { | |
if ($s["mtime"] < time()) { | |
$this->unlink($this->dir.$file); | |
$i++; | |
} | |
} | |
} | |
closedir($dh); | |
} | |
//error_log("File cache garbage collected: $i items removed"); | |
} | |
/** | |
* Store a variable in the cache. | |
* | |
* Store any serializable variable in the cache. Note that it is not | |
* possible to overwrite an existing entry (cache slam). Such an event | |
* will not raise an error but the function will report it. | |
* | |
* @param \Scrivo\String $key A cache unique name for the key. | |
* @param mixed $val The (serializable) variabele to strore. | |
* @param int $ttl Time to live in seconds. | |
* | |
* @return int DATA_STORED if the variable was succesfully stored, | |
* CACHE_SLAM if key already exists or NO_SPACE if there is not | |
* enough space left to store the value. | |
* | |
* @throws \Scrivo\SystemException When trying to store a NULL value or | |
* when a file operation fails. | |
*/ | |
public function store(\Scrivo\String $key, $val, $ttl=3600) { | |
if ($val === null) { | |
throw new \Scrivo\SystemException( | |
"Can't store null values in the cache"); | |
} | |
// The name for the cache file. | |
$file = $this->getFile($key); | |
// If the file already exists, and other thread is already writing | |
// the file and we're done here. | |
if (file_exists($file)) { | |
return self::CACHE_SLAM; | |
} | |
// Run the garbage collector at the specified probability. | |
if (($this->storeCount % $this->gcInterval) == $this->gcInterval - 1) { | |
$this->gc(); | |
} | |
// Get the data to store. | |
$data = new \Scrivo\ByteArray(serialize($val)); | |
// Create the file and bail out not succesfull. | |
$fp = $this->fopen($file); | |
// Write the data. | |
if (fwrite($fp, (string)$data) != $data->length) { | |
// Data was not fully written: drop infreq. used and unused entries. | |
fclose($fp); | |
$this->delete($key); | |
$this->purge($this->pctToKeepAfterPurge); | |
// try again | |
$fp = $this->fopen($file); | |
if (fwrite($fp, (string)$data) != $data->length) { | |
// Data was not fully written again: close and remove the file. | |
fclose($fp); | |
$this->delete($key); | |
return self::NO_SPACE; | |
} | |
} | |
// Now we have a file touch it. | |
fclose($fp); | |
$this->touch($file, $ttl); | |
$this->storeCount++; | |
return self::DATA_STORED; | |
} | |
/** | |
* Store a variable in the cache, overwrite it if it already exists. | |
* | |
* Store any serializable variable in the cache. It is guaranteed that | |
* the data will be written. But note that it is not guaranteed that the | |
* next fetch will retrieve this value. | |
* | |
* @param \Scrivo\String $key A cache unique name for the key. | |
* @param mixed $val The (serializable) variabele to strore. | |
* @param int $ttl Time to live in seconds. | |
* | |
* @return int DATA_STORED if the variable was succesfully stored or | |
* NO_SPACE if there is not enough space left to store the value. | |
* | |
* @throws \Scrivo\SystemException When trying to store a NULL value or | |
* when a file operation fails. | |
*/ | |
public function overwrite(\Scrivo\String $key, $val, $ttl=3600) { | |
// Theoretically just after delete an other thread can store a value. | |
for ($tmp=self::CACHE_SLAM; $tmp==self::CACHE_SLAM;) { | |
$this->delete($key); | |
$tmp = $this->store($key, $val, $ttl); | |
} | |
return $tmp; | |
} | |
/** | |
* Delete/remove a cache entry. | |
* | |
* @param \Scrivo\String $key A cache unique name for the key. | |
*/ | |
public function delete(\Scrivo\String $key) { | |
$file = $this->getFile($key); | |
if (file_exists($file)) { | |
$this->unlink($file); | |
} | |
} | |
/** | |
* Retrieve a value from the cache. | |
* | |
* @param \Scrivo\String $key The key for which to retrieve the value. | |
* | |
* @return mixed The value of the stored variable or NULL if the key | |
* does not exists or is expired. | |
*/ | |
public function fetch(\Scrivo\String $key) { | |
$file = $this->getFile($key); | |
$res = null; | |
// Suppressing the message is probably faster then calling file_exists | |
// and stat. | |
if ($s = @stat($file)) { | |
if ($s["mtime"] < time()) { | |
$this->delete($key); | |
} else { | |
$tmp = file_get_contents($file); | |
// if (strtoupper(substr(PHP_OS,0,3))==='WIN') | |
// touch($file,date('U',filemtime($file)),time()); | |
// | |
if ($tmp) { | |
$res = unserialize($tmp); | |
} | |
} | |
} | |
return $res ? $res : null; | |
} | |
/** | |
* List all entries in the cache. | |
* | |
* The cache list is an array in which the cache keys are the keys of | |
* the array entries and the data of the entries are objects ot type | |
* stdClass that contain at least contain the following properties: | |
* | |
* * accessed: the access time | |
* * expires: the expiration time | |
* * created: the creation time | |
* * size: the size of the entry | |
* | |
* @return object[] A array of objects that describe the current cache | |
* entries. | |
*/ | |
public function entryList() { | |
$l = array(); | |
if ($dh = opendir($this->dir)) { | |
while (($file = readdir($dh)) !== false) { | |
$s = stat($this->dir.$file); | |
if (!($s['mode'] & 040000)) { | |
$k = \Scrivo\String::create($file)->replace( | |
$this->escapedCharacters, $this->reservedCharacters); | |
$l[(string)$k] = (object)array( | |
"accessed" => $s["atime"], | |
"expires" => $s["mtime"], | |
"created" => $s["ctime"], | |
"size" => $s["size"] | |
); | |
} | |
} | |
closedir($dh); | |
} | |
return $l; | |
} | |
} | |