Primary cache in Kohana 3 using tags

The given example is the result of solving one problem that arose during the development of the content management system on the Kohana 3.1 framework, in which one administrator account and many unregistered readers are assumed.

It took a long time to cache the results of model methods that access the database. In fact, it was necessary to create copies of the data sets from the database in order to reduce the load on the DBMS. To immediately update the cache when adding new data or updating old ones, it was necessary to clear the cache by tags.

Given all this, and due to the limitations of the hosting used, the requirements were as follows:
  • The cache must be stored in files.
  • The cache should be stored for a long time to increase the speed of data extraction and reduce the load on the DBMS.
  • When updating the data by the site administrator, the cache containing outdated data should be cleared, and not only the result cache of functions directly extracting this data from the database should be cleared, but also those whose results are associated with this data (for example, when deleting the catalog section, it should be cleared cache of lists of headings for categories). To achieve this, tags must be supported.
  • In order to achieve the goal, you can sacrifice, within a certain framework, the time taken to add new materials, which will be spent including clearing the cache, as they are added “by your own person” and not by third-party users.

The standard class Cache_Filedoes not support tags, for this reason it was required to write its own class, it was given a name JetCache.

The class is designed according to the "loner" template. Consider an example of a class working in a model for a file bank. When the model is initialized, an instance is created:

$this->cache = JetCache::instance();


We will consider the creation of a data cache using an example of a function for extracting a list of files of a certain category (here some arguments and some code are removed from it to simplify reading):

//Вернуть список файлов рубрики/**
     *
     * @param int $rubricId Id рубрики в БД
     * @return array
     */publicfunctiongetFiles($rubricId){
        //!!! В случае, если удается получить информацию из кэша, //вернуть эту информацию
        $key = 'filebank_get_files'.$rubricId;
        $arResult = $this->cache->get($key);
        if (is_array($arResult)) {
            return $arResult;
        }
        $arResult = array();
        $arParams = array();
        $arParams[':rubricId'] = $rubricId;
        $query = "
            SELECT 
                * 
            FROM 
                `filebank_files` 
            WHERE 
                `rubric_id`=:rubricId 
            ORDER BY 
                `time` DESC, 
                `name` ASC
        ";
        $arResult['files'] = DB::query(Database::SELECT, $query)
                ->parameters($arParams)
                ->execute()
                ->as_array();
        //!!! Внести в кэш результат работы$this->cache->set($key, $arResult, array('filebank_rubrics', 'filebank_files'));
        return $arResult;
    }


Thus, an entry to the cache is made with the key $key = 'filebank_get_files'.$rubricIdand the tags “filebank_rubrics” and “filebank_files”, that is, you need to clear this entry when updating information about categories and files directly.

For an example of clearing the cache by tags, consider a function to delete a category. The property cacheRegExpcontains a regular expression for the names of files (keys) from which to retrieve tags for verification. That is, the check is double: first check the file name by regular expression, then check the tags.

protected $cacheRegExp = '/^filebank/';
    //Удалить рубрикуpublicfunctiondelRubric($rubricId){
        $query = '
            SELECT 
                `name` 
            FROM 
                `filebank_files` 
            WHERE 
                `rubric_id`=:rubricId
        ';
        $arFiles = DB::query(Database::SELECT, $query)
                ->param(':rubricId', $rubricId)
                ->execute()
                ->as_array();
        $arFiles = Arr::path($arFiles, '*.name');
        $this->delFiles($arFiles);
        $query = '
            DELETE FROM 
                `filebank_rubrics` 
            WHERE 
                `rubric_id`=:rubricId
        ';
        DB::query(Database::DELETE, $query)
                ->param(':rubricId', $rubricId)
                ->execute();
        //!!! Очистка кэша по тегам
        $tags = array('filebank_rubrics', 'filebank_files');
        $this->cache->delete_by_tags($tags, $this->cacheRegExp);
    }

That is, after deleting the rubric, the cache of data associated with rubrics and positions in rubrics is cleared.

Thus, the possibility of primary long-term data caching was added. At the controller level, you can also use short-term caching using the standard "Cache" module with a file driver.

In the case of large projects where a high speed of clearing the cache or adding information by users is required, of course, it is better to use special solutions. For example, the drivers "Memcached-tag" or "Xcache" module "Cache". But for small sites administered by one person or a small group of people, when using hosting without providing special caching tools, this solution works well.

The files in which the cache is stored are contained in one directory and have the following structure: Finally, I will give the full class code:
Время, после которого запись считается устаревшей (unix timestamp)\n
Список тегов через запятую\n
Сериализованные данные\n



<?php defined('SYSPATH') ordie('No direct access allowed.');
classJetCache{
    protectedstatic $instance = NULL;
    protectedstatic $config;
    protectedstatic $cache_dir;
    protectedstatic $cache_time;
    publicstaticfunctioninstance(){
        if (is_null(self::$instance)) {
            self::$instance = newself();
        }
        returnself::$instance;
    }
    protectedfunction__construct(){
        self::$config = Kohana::config('jethelix')->default;
        self::$cache_dir = self::$config['jet_cache_dir'];
        if (!is_dir(self::$cache_dir)) 
        {
            $oldUmask = umask(0000);
            if (!mkdir(self::$cache_dir, 0777, TRUE)) {
                $message = 'Неверная директория для модуля JetCache';
                thrownewException($message);
            }
            umask($oldUmask);
        }
        self::$cache_time = self::$config['jet_cache'];
    }
    protectedfunction__clone(){
    }
    publicfunctionset($id, $data, array $tags=array(), $lifetime=NULL){
        if (!$lifetime) {
            $lifetime = self::$cache_time;
        }
        $filename = self::$cache_dir . '/' . $id . '.txt';
        $expires = time() + (int)$lifetime;
        $tagString = implode(',', $tags);
        $serData = serialize($data);
        $content = $expires . "\n" . $tagString . "\n" . $serData;
        try {
            file_put_contents($filename, $content);
        }
        catch (Exception $e) {
            returnFALSE;
        }
        returnTRUE;
    }
    publicfunctionget($id){
        $filename = self::$cache_dir . '/' . $id . '.txt';
        if (!is_file($filename)) {
            returnNULL;
        }
        try {
            $content = file_get_contents($filename);
        }
        catch (Exception $e) {
            returnNULL;
        }
        $arContent = explode("\n", $content);
        unset ($content);
        try {
            if ($arContent[0] < time()) {
                returnNULL;
            }
            $data = unserialize($arContent[2]);
            return $data;
        }
        catch (Exception $e) {
            returnNULL;
        }
    }
    publicfunctiondelete($id){
        $filename = self::$cache_dir . '/' . $id . '.txt';
        try {
            unlink($filename);
        }
        catch (Exception $e) {
            returnFALSE;
        }
        returnTRUE;
    }
    publicfunctiongarbage_collect(){
        $dir = opendir(self::$cache_dir);
        while ($file = readdir($dir)) 
        {
            $fullName = self::$cache_dir . '/'. $file;
            if (!is_file($fullName)) {
                continue;
            }
            try {
                $this->_deleteIfExpires($fullName);
            }
            catch (Exception $e) {
                returnFALSE;
            }
        }
        returnTRUE;
    }
    protectedfunction_deleteIfExpires($filename){
        $fhandle = fopen($filename, 'r');
        $expires = (int)fgets($fhandle);
        fclose($fhandle);
        if ($expires < time()) {
            unlink($filename);
        }
    }
    publicfunctiondelete_by_tags(array $tags, $filenameRegExp=NULL){
        $this->garbage_collect();
        try {
            $arFiles = $this->_getTaggedFiles($tags, $filenameRegExp);
            $this->_deleteFiles($arFiles);
        }
        catch (Exception $e) {
            returnFALSE;
        }
        returnTRUE;
    }
    protectedfunction_getTaggedFiles(array $needTags, $filenameRegExp){
        $taggedFiles = array();
        $dir = opendir(self::$cache_dir);
        while ($file = readdir($dir)) 
        {
            $fullName = self::$cache_dir . '/' . $file;
            if (!is_file($fullName)) {
                continue;
            }
            if ($filenameRegExp && !preg_match($filenameRegExp, $file)) {
                continue;
            }
            $hasTags = $this->_getTagsFromFile($fullName);            
            $isValid = $this->_tagsValidate($needTags, $hasTags);
            if ($isValid) {
                $taggedFiles[] = $fullName;
            }
        }
        return $taggedFiles;
    }
    protectedfunction_getTagsFromFile($filename){
        $fhandler = fopen($filename, 'r');
        fgets($fhandler);
        $tagString = fgets($fhandler);
        fclose($fhandler);
        $tagString = trim($tagString);
        $arTags = explode(',', $tagString);
        return $arTags;
    }
    protectedfunction_tagsValidate(array $needTags, array $hasTags){
        foreach ($needTags as $tag) {
            if (in_array($tag, $hasTags)) {
                returnTRUE;
            }
        }
        returnFALSE;
    }    
    protectedfunction_deleteFiles(array $files){
        foreach ($files as $filename) {
            unlink($filename);
        }
    }
}

Also popular now: