layh.com

Whatever...TYPO3...FLOW3...and more...

Extend the TYPO3 Caching Framework with an own frontend and backend

Some time ago I worked on an extbase extension that uses web service calls to get the required data. After the data is received from the web service, I build objects out of the data. The object are cached using the TYPO3 caching framework. Since there was the problem that the web service is not always available it was required to have some kind of fallback. If a web service call fails, I wanted to have a fallback to an old, even to an outdated entry. For this fallback I had to extend the caching framework a little bit to have this fallback available.

There is a good article about the usage of the caching framework and I just link to the page of Daniel Lienert. The post is in German but the snippets should be enough to get an idea how to use the caching framework.

http://daniel.lienert.cc/blog/blog-post/2011/03/12/typo3-caching-framework-in-eigener-extension-verwenden/

Extend the class t3lib_cache_backend_DbBackend

I highlighted the interesting lines in the following code snippets. Changes that have been important for my case are explained below.

Line 3-4: This is just a switch that tells me if I should ignore the cache lifetime or if it is ok to check for old cache entries. If there is no web service available and there are no valid entries I can set this to true to get also the old entries.

Line 12: Overwrite the function has here.

Line 14-20: Condition to check if we respect the lifetime for cache entries or not.

Line 25: The lifetime condition I created in line 14-20 is attached to the check at the end of the line.

Line 39-44: Setter function for the lifetime switch.

Line 52: Here we overwrite the get-method. We can completely ignore the lifetime here. We overwrite the set-method too, so we can be sure that there is only one cache entry for each identifier. We use an insert or update for the set function. 

Line 77: Overwrite the set-method. At the end of the method we call the function insertOrUpdateCache that takes care of the job that normally is done by the set function.

Line 98: insertOrUpdateCache: If there is already an entry for the current identifier, we just update the cache record. Otherwise we add a new entry.

Extend the class t3lib_cache_backend_DbBackend
class Tx_MyExtension_System_Cache_BackendDB extends t3lib_cache_backend_DbBackend {

	/** @var boolean */
	private $useOldEntry = false;

	/**
	 * Check if the key exists
	 *
	 * @param string $entryIdentifier
	 * @return boolean
	 */
	public function has($entryIdentifier) {

		$cacheEntry = false;

		if ($this->useOldEntry) {
			$lifetimeCondition = '';
		} else {
			$lifetimeCondition = ' AND (crdate + lifetime >= ' . $GLOBALS['EXEC_TIME'] . ' OR lifetime = 0)';
		}

		$cacheEntries = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
			'*',
			$this->cacheTable,
			'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable) . $lifetimeCondition,
			'',
			'crdate DESC', // order by crdate
			'0,1' // limit to one entry
		);

		if (count($cacheEntries) == 1) {
			$cacheEntry = true;
		}

		return $cacheEntry;
	}

	/**
	 * @param boolean $useOldEntries
	 * @return void
	 */
	public function setCheckOldEntries($useOldEntries) {
		$this->useOldEntry = $useOldEntries;
	}

	/**
	 * Get the value for the current key
	 *
	 * @param string $entryIdentifier
	 * @return mixed
	 */
	public function get($entryIdentifier) {
		$cacheEntry = false;
		$cacheEntries = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
			'*',
			$this->cacheTable,
			'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable),
			'',
			'crdate DESC', // order by crdate
			'0,1' // limit to one entry
		);

		// get the cache entry
		if (count($cacheEntries) == 1) {
			$cacheEntry = $cacheEntries['0']['content'];
		}
		return $cacheEntry;
	}

	/**
	 * @param string $entryIdentifier
	 * @param mixed $data
	 * @param array $tags
	 * @param int $lifetime
	 * @return boolean
	 */
	public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
		if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
			throw new t3lib_cache_Exception('No cache frontend has been set via setCache() yet.', 1236518288);
		}
		if (!is_string($data)) {
			throw new t3lib_cache_exception_InvalidData('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1236518298);
		}
		if (is_null($lifetime)) {
			$lifetime = $this->defaultLifetime;
		}


		$this->insertOrUpdateCache($entryIdentifier, $data, $lifetime);
	}

	/**
	 * @param string $entryIdentifier
	 * @param mixed $data
	 * @param int $lifetime
	 * @return boolean
	 */
	private function insertOrUpdateCache($entryIdentifier, $data, $lifetime) {
		$cacheEntries = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('identifier', $this->cacheTable, 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable));

		$fieldValues = array('crdate' => $GLOBALS['EXEC_TIME'], 'content' => $data, 'lifetime' => $lifetime);

		if (count($cacheEntries) > 0) {
			$GLOBALS['TYPO3_DB']->exec_UPDATEquery($this->cacheTable, 'identifier=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable), $fieldValues);
		} else {
			$fieldValues['identifier'] = $entryIdentifier;
			$GLOBALS['TYPO3_DB']->exec_INSERTquery($this->cacheTable, $fieldValues);
		}
	}

}

Extend the class t3lib_cache_frontend_VariableFrontend

We have to overwrite the frontend too. We also overwrite the has-method to take care of the lifetime and so we also have to implement the setCheckOldEntries-method here. This function is used from the frontend as well as from the backend.

Line 3: Again the switch that decides if we respect the lifetime of an entry or not.

Line 8-14: The method to set if we respect the lifetime or not.

Line 22: The has-method. Similar to the backend function we have to check here if we ignore the lifetime or not.

Extend the class t3lib_cache_frontend_VariableFrontend
class Tx_MyExtension_System_Cache_VariableFrontend extends t3lib_cache_frontend_VariableFrontend {

	/** @var boolean */
	private $useOldEntry = false;

	private $cacheTable = 'tx_myxtension_cache';

	/**
	 * @param boolean $useOldEntries
	 * @return void
	 */
	public function setCheckOldEntries($useOldEntries) {
		$this->useOldEntry = $useOldEntries;
	}

	/**
	 * Check if the key exists
	 *
	 * @param string $entryIdentifier
	 * @return boolean
	 */
	public function has($entryIdentifier) {

		$cacheEntry = false;

		if($this->useOldEntry) {
			$lifetimeCondition = '';
		} else {
			$lifetimeCondition = ' AND (crdate + lifetime >= ' . $GLOBALS['EXEC_TIME'] . ' OR lifetime = 0)';
		}

		$cacheEntries = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
			'*',
			$this->cacheTable,
			'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable) .
			$lifetimeCondition,
			'',
			'crdate DESC',	// order by crdate
			'0,1'			// limit to one entry 
		);

		if(count($cacheEntries) == 1) {
			$cacheEntry = true;
		}

		return $cacheEntry;
	}

}

Implement a wrapper-class to use the caching framework

This class is between your application and the caching framework. The class does nothing more than to forward the method calls to either the frontend or the backend of the caching framework.

We also  have to implement the setter-function for the lifetime check here to pass the variable to the caching framework.

Important in this class is the initialization of the caching framework. Therefor we have the method initializeLevel1Cache. Here we have to set our own frontend and backend in the configuration parameters in Line 69 and 70.

 

Usage of the caching framework
class Tx_MyExtension_System_Cache_MySpecialCache {

	/**
	 * @var \Tx_MyExtension_System_Cache_BackendDB
	 */
	private $cache;

	/**
	 * constructor
	 */
	public function __construct() {
		$this->initializeLevel1Cache();
	}

	/**
	 * @param boolean $checkOldEntries
	 * @return void
	 */
	public function setCheckOldEntries($checkOldEntries) {
		$this->cache->setCheckOldEntries($checkOldEntries);
	}

	/**
	 * Check for the cache entry
	 * @param string $id
	 * @return bool
	 */
	public function has($id) {
		return $this->cache->has($id);
	}

	/**
	 * Get the cache entry
	 * @param string $id
	 * @return array
	 */
	public function get($id) {
		return $this->cache->get($id);
	}

	/**
	 * Generate a unique cache id
	 * @param string $url
	 * @return string $cacheId
	 */
	public function generateCacheId($url) {
		return md5($url);
	}

	/**
	 * set the cache for the id
	 * @param string $id
	 * @param mixed $value
	 * @return void
	 */
	public function set($id, $value) {
		if(count($value) > 0) {
			$this->cache->set($id, $value);
		}
	}

	/**
	 * Initialize TYPO3 caching framework
	 * @return void
	 */
	private function initializeLevel1Cache() {
		t3lib_cache::initializeCachingFramework();
		// Create the cache
		$frontend = 'Tx_MyExtension_System_Cache_VariableFrontend';
		$backend = 'Tx_MyExtension_System_Cache_BackendDB';
		$config = array('defaultLifetime' => 300,
						'cacheTable' => 'tx_myextensiom_cache',
						'tagsTable' => 'tx_myextension_cache_tags');

		// create the cache if it doesn't exists
		try {
			$GLOBALS['typo3CacheFactory']->create('my_extension', $frontend, $backend, $config);
		} catch (t3lib_cache_exception_DuplicateIdentifier $e) {
			// do nothing, the cache already exists
		}
		
		// Initialize the cache
		try {
			$this->cache = $GLOBALS['typo3CacheManager']->getCache('my_extension');
		} catch (t3lib_cache_exception_NoSuchCache $e) {
			// Unable to load
		}

	}

}