Skip to content

Commit

Permalink
Issue #159 : caching system
Browse files Browse the repository at this point in the history
* 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 27, 2014
1 parent c976a45 commit 4515974
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 14 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -10,3 +10,5 @@ web/docs
app/config/config.ini
web/p12n
web/TMX
cache/*.cache
cache/lastdataupdate.txt
2 changes: 1 addition & 1 deletion app/classes/Transvision/Bugzilla.php
Expand Up @@ -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));
Expand Down
168 changes: 168 additions & 0 deletions app/classes/Transvision/Cache.php
@@ -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);
}
}
10 changes: 9 additions & 1 deletion app/inc/constants.php
Expand Up @@ -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);
Expand Down
32 changes: 22 additions & 10 deletions app/models/onestring.php
Expand Up @@ -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);
7 changes: 7 additions & 0 deletions glossaire.sh
Expand Up @@ -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
72 changes: 72 additions & 0 deletions tests/units/Transvision/Cache.php
@@ -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)
;
}
}
3 changes: 1 addition & 2 deletions tests/units/bootstrap.php
@@ -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.