Permalink
Browse files

Issue #159 : caching system

* New Cache class with these methods:
Public:
 - setKey()
 - getKey()
 - flush()

Private:
 - deleteKey()
 - isValidKey()
 - isObsoleteKey()
 - getKeyPath()
 - getCachePath()
 - isActivated()

* New constants:
 - CACHE_ENABLED activates/deactivates the cache
 - CACHE_TIME is the default duration of the cache
 - CACHE_PATH is the path to the cache folder

There are also fallback constants in the class to use it outside of Transvision as a drop-in class.

* GET['nocache'] to force a page not to use caching

* Cache duration
 - age of the last generation of data (uses a timestamp file in glossaire.sh), defaults to 5h20 if the timestamp file is missing
 - can be defined in seconds when using getKey(), ex getKey($id, 300)

* Onestring model makes a use of the class, ex:

if (! $data = Cache::getKey($cache_id)) {
    /* create $data */
    /* Now cache data */
    Cache::setKey($cache_id, $data);
}

* Tests for all methods
  • Loading branch information...
pascalchevrel committed Mar 21, 2014
1 parent c976a45 commit 4515974b566ecf7ebb5b4e6e5bebcefc0927f102
View
@@ -10,3 +10,5 @@ web/docs
app/config/config.ini
web/p12n
web/TMX
+cache/*.cache
+cache/lastdataupdate.txt
@@ -18,7 +18,7 @@ class Bugzilla
*/
public static function getBugzillaComponents()
{
- $cache_file = CACHE . 'bugzilla_components.json';
+ $cache_file = CACHE_PATH . 'bugzilla_components.json';
if (!file_exists($cache_file)) {
$json_url = 'https://bugzilla.mozilla.org/jsonrpc.cgi?method=Product.get&params=[%20{%20%22names%22:%20[%22Mozilla%20Localizations%22]}%20]';
file_put_contents($cache_file, file_get_contents($json_url));
@@ -0,0 +1,168 @@
+<?php
+namespace Transvision;
+
+/**
+ * Cache class
+ *
+ * A simple and fast file caching system.
+ *
+ * 3 global constants are used: CACHE_ENABLED, CACHE_PATH and CACHE_TIME
+ * If those app constants are not available, the system temp folder
+ * and the class constants CACHE_ENABLED and CACHE_TIME are used.
+ *
+ * @package Transvision
+ */
+class Cache
+{
+ /** Fallback for activation of Cache */
+ const CACHE_ENABLED = true;
+
+ /** Duration of the cache */
+ const CACHE_TIME = 3600;
+
+ /**
+ * Create a cache file with serialized data
+ *
+ * We use PHP serialization and not json for example as it allows
+ * storing not only data but also data representations and
+ * instantiated objects.
+ *
+ * @param string $id
+ * @param object $data
+ * @return boolean True if cache file is created. False if there was an error.
+ */
+ public static function setKey($id, $data)
+ {
+ if (! self::isActivated()) {
+ return false;
+ }
+
+ return file_put_contents(self::getKeyPath($id), serialize($data)) ? true : false;
+ }
+
+ /**
+ * Get the cached serialized data via its UID
+ *
+ * @param string $id UID of the cache
+ * @param int $ttl Number of seconds for time to live. Default to 0.
+ * @return object Unserialized cached data. Or False
+ */
+ public static function getKey($id, $ttl = 0)
+ {
+ if (! self::isActivated()) {
+ return false;
+ }
+
+ if ($ttl == 0) {
+ $ttl = defined('CACHE_TIME') ? CACHE_TIME : self::CACHE_TIME;
+ }
+
+ return self::isValidKey($id, $ttl)
+ ? unserialize(file_get_contents(self::getKeyPath($id)))
+ : false;
+ }
+
+ /**
+ * Flush our cache
+ *
+ * @return boolean True if files in cache are deleted, False if some files were not deleted
+ */
+ public static function flush()
+ {
+ $files = glob(self::getCachePath() . '*.cache');
+
+ return ! in_array(false, array_map('unlink', $files));
+ }
+
+ /**
+ * Is the caching system activated?
+ * We look at the existence of a CACHE constant and if it's at True
+ *
+ * @return boolean True if activated, False if deactivated
+ */
+ private static function isActivated()
+ {
+ return defined('CACHE_ENABLED') ? CACHE_ENABLED : self::CACHE_ENABLED;
+ }
+
+ /**
+ * Check if cached data for a key is usable
+ *
+ * @param string $id UID for the data
+ * @param int $ttl Number of seconds for time to live
+ * @return boolean if valid data, false if no usable cached data
+ */
+ private static function isValidKey($id, $ttl)
+ {
+ // No cache file
+ if (! file_exists(self::getKeyPath($id))) {
+ return false;
+ }
+
+ // Cache is obsolete and was deleted
+ if (self::isObsoleteKey($id, $ttl)) {
+ self::deleteKey($id);
+ return false;
+ }
+
+ // All good, cache is valid
+ return true;
+ }
+
+ /**
+ * Delete a cache file thanks to its UID
+ *
+ * @param string $id UID of the cached data
+ * @return boolean True if data was deleted, false if it doesn't exist
+ */
+ private static function deleteKey($id)
+ {
+ $file = self::getKeyPath($id);
+
+ if (! file_exists($file)) {
+ return false;
+ }
+
+ unlink($file);
+ clearstatcache(true, $file);
+
+ return true;
+ }
+
+ /**
+ * Get the path to the cached file
+ *
+ * File is of the form a840d513be5240045ccc979208f739a168946332.cache
+ *
+ * @param string $id UID of the cached file
+ * @return string path for the file
+ */
+ public static function getKeyPath($id)
+ {
+ return self::getCachePath() . sha1($id) . '.cache';
+ }
+
+ /**
+ * Get the path to the cache folder
+ *
+ * If a CACHE_PATH global constant is defined, use it,
+ * otherwise write to OS folder for temporary files.
+ *
+ * @return string path to Cache
+ */
+ private static function getCachePath()
+ {
+ return defined('CACHE_PATH') ? CACHE_PATH : sys_get_temp_dir() . '/';
+ }
+
+ /**
+ * Check if the data has not expired
+ * @param string $id UID of the cached file
+ * @param int $ttl Number of seconds for time to live
+ * @return boolean True if file is obsolete, False if it is still fresh
+ */
+ private static function isObsoleteKey($id, $ttl)
+ {
+ return filemtime(self::getKeyPath($id)) < (time() - $ttl);
+ }
+}
View
@@ -15,7 +15,15 @@
define('VIEWS', APP_ROOT . 'views/');
define('MODELS', APP_ROOT . 'models/');
define('CONTROLLERS', APP_ROOT . 'controllers/');
-define('CACHE', INSTALL_ROOT . 'cache/');
+define('CACHE_ENABLED', isset($_GET['nocache']) ? false : true);
+define('CACHE_PATH', INSTALL_ROOT . 'cache/');
+
+if (file_exists(CACHE_PATH . 'lastdataupdate.txt')) {
+ define('CACHE_TIME', time() - filemtime(CACHE_PATH . 'lastdataupdate.txt'));
+} else {
+ // 05h20 cache (because we extract data every 6h and extraction lasts 25mn)
+ define('CACHE_TIME', 19200);
+}
// Special modes for the app
define('DEBUG', (strstr(VERSION, 'dev') || isset($_GET['debug'])) ? true : false);
View
@@ -14,22 +14,34 @@
$entity = isset($_GET['entity']) ? $_GET['entity'] : false;
// Invalid entity, we don't do any calculation and get back to the view
-if (!$entity) {
+if (! $entity) {
return $error = 1;
} elseif (!array_key_exists($entity, $strings)) {
return $error = 2;
}
-if ($repo != 'mozilla_org') {
- $translations = ['en-US' => $strings[$entity]];
-}
+$cache_id = $repo . $entity . 'alllocales';
+
+if (! $translations = Cache::getKey($cache_id)) {
-foreach(Files::getFilenamesInFolder(TMX . $repo . '/', ['ab-CD']) as $localecode) {
- $strings = Utils::getRepoStrings($localecode, $repo);
- if (array_key_exists($entity, $strings)) {
- $translations[$localecode] = $strings[$entity];
- } else {
- $translations[$localecode] = false;
+ if ($repo == 'mozilla_org') {
+ // we always want to have an en-US locale for the Json API
+ $translations = ['en-US' => $strings[$entity]];
}
+
+ foreach (Files::getFilenamesInFolder(TMX . $repo . '/', ['ab-CD']) as $locale_code) {
+
+ $strings = Utils::getRepoStrings($locale_code, $repo);
+
+ if (array_key_exists($entity, $strings)) {
+ $translations[$locale_code] = $strings[$entity];
+ } else {
+ $translations[$locale_code] = false;
+ }
+ }
+
+ Cache::setKey($cache_id, $translations);
}
+
+
unset($strings);
View
@@ -295,3 +295,10 @@ then
echogreen "Create L20N test repo TMX for en-US"
nice -20 python tmxmaker.py $l20n_test/l20ntestdata/en-US/ $l20n_test/l20ntestdata/en-US/ en-US en-US l20n_test
fi
+
+# Create a file to get the timestamp of the last string extraction for caching
+echogreen "Creating extraction timestamp for cache system"
+touch cache/lastdataupdate.txt
+
+echogreen "Deleting all the old cached files"
+rm -f cache/*.cache
@@ -0,0 +1,72 @@
+<?php
+namespace Transvision\tests\units;
+use atoum;
+
+require_once __DIR__ . '/../bootstrap.php';
+
+class Cache extends atoum\test
+{
+
+ public function beforeTestMethod($method)
+ {
+ // Executed *before each* test method.
+ switch ($method)
+ {
+ case 'testFlush':
+ // Prepare testing environment for testFlush().
+ $files = new \Transvision\Cache();
+ // create a few files to delete
+ $files->setKey('file_1', 'foobar');
+ $files->setKey('file_2', 'foobar');
+ $files->setKey('file_3', 'foobar');
+ break;
+
+ case 'testGetKey':
+ // Prepare testing environment for testGetKey().
+ $files = new \Transvision\Cache();
+ $files->setKey('this_test', 'foobar');
+ // Change the timestamp to 100 seconds in the past so we can test expiration
+ touch(CACHE_PATH . sha1('this_test') . '.cache', time()-100);
+ break;
+ }
+ }
+
+ public function testSetKey()
+ {
+ $obj = new \Transvision\Cache();
+ $this
+ ->boolean($obj->setKey('this_test', 'foobar'))
+ ->isEqualTo(true)
+ ;
+ }
+
+ public function getKeyDP()
+ {
+ return array(
+ ['this_test', 0, 'foobar'], // valid key
+ ['this_test', 2, false], // expired key
+ ['id_that_doesnt_exist', 0, false], // non-existing key
+ );
+ }
+
+ /**
+ * @dataProvider getKeyDP
+ */
+ public function testGetKey($a, $b, $c)
+ {
+ $obj = new \Transvision\Cache();
+ $this
+ ->variable($obj->getKey($a, $b))
+ ->isEqualTo($c)
+ ;
+ }
+
+ public function testFlush()
+ {
+ $obj = new \Transvision\Cache();
+ $this
+ ->boolean($obj->flush())
+ ->isEqualTo(true)
+ ;
+ }
+}
@@ -1,7 +1,6 @@
<?php
$ini_array = parse_ini_file(__DIR__ . '/../../app/config/config.ini');
define('TMX', $ini_array['root'] . '/TMX/');
-define('CACHE', __DIR__ . '/../testfiles/cache/');
-
+define('CACHE_PATH', realpath(__DIR__ . '/../testfiles/cache/') . '/');
require __DIR__.'/../../vendor/autoload.php';

0 comments on commit 4515974

Please sign in to comment.