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; | |
| } | |
| } | |