Cache.php

<?php

namespace Lia\Compo;

class Cache extends \Lia\Compo {

/**
 *
 * - Consider adding defaultExpiry as a config, rather than a default paramater in the cacheFile() function call
 * - Add a key=>value cache that returns values instead of file paths
 * - probably auto-delete if an expired file is requested
 * - Possibly add `lia.cacheDir.public` & `lia.cacheDir.public.baseUrl` to assist in routing
 * - Are these the same thing?
 *     - Consider function to return file content, not just file path. Consider additional decoding like parsing json or yaml.
 *     - Consider adding type-based processing (PHP, JSON, DOMDocument??)
 *     - Consider extensible features (such as auto en/decoding additional types not supported by Liaison)
 * - cache response of getCacheFile() so files don't have to be loaded more than once
 * 
 * @export(TODO.Cache)
 */

    public function onStart(){
        parent::onStart();
        if ($this->lia->hasApi('lia.conf')){
            $this->lia->api('lia.conf', 'default', 'lia.cacheDir', $this->package->dir('cache'));
            // delete stuff every 5 days
            $this->lia->api('lia.conf', 'default', 'lia.Cache.clearAfterXMinutes', 60*24*5);
        }
    }

    public function onEmit_Server_responseSent(){
        if ($this->isStaleCacheCheckRequired()){
            $this->deleteStaleCacheFiles();            
        }
    }

    public function apiDeprecated_lia_cache_getCacheFile($key){
        $path = $this->lia->getCachedFilePath($key);
        return $path;
    }
    /**
     * 
     * @param string $key
     * @return boolean|string, false when cache file is not found or is stale, else absolute path to the cache file
     */
    public function apiGet_lia_cache_getCachedFilePath(string $key){

        $dir = $this->lia->get('lia.cacheDir');
        $file = $dir.'/file-'.$key;
        $metaFile = $dir.'/meta-'.$key;
        
        if (file_exists($file)
            &&file_exists($metaFile)){
                
                $meta = json_decode(file_get_contents($metaFile),true);

                if (time()>$meta['expiry']){
                    //@TODO Auto-delete stale cache files when requested
                    return false;
                }
                return $file;
        } else {
            return false;
        }
    }

    /**
     * Store the given $value in a file identified by $key
     * Returns the path to the file
     */
    public function apiSet_lia_cache_cacheFile($key, $value, $maxAge=60*60*24*5){
        $dir = $this->lia->get('lia.cacheDir');
        $file = $dir.'/file-'.$key;
        $metaFile = $dir.'/meta-'.$key;

        $parent = $dir;
        $hasDir = true;
        while (!is_dir($parent)){
            $hasDir = false;
            $parent = dirname($parent);
        }
        $perms = fileperms($parent);
        if (!$hasDir)mkdir($dir, $perms, true);

        file_put_contents($file,$value);
        $expiry = time() + $maxAge;
        $meta = [
            'expiry'=>$expiry,
        ];
        file_put_contents($metaFile,json_encode($meta));
        return $file;
    }
    
    
    protected function isStaleCacheCheckRequired(){
        // get cache-clearing frequency (lia config)
        $frequencyInMinutes = $this->lia->get('lia.Cache.clearAfterXMinutes');
        $seconds = $frequencyInMinutes*60;
        // check when last cache-clear was performed
        $path = $this->lia->getCachedFilePath('lia.Cache.LastCleanupCheck');
        if ($path==null||!is_file($path)){
            $lastClear = 0;
        } else if (is_file($path)){
            $content = file_get_contents($path);
            $content = trim($content);
            $lastClear = (int)$content;
        } else {
            throw new \Lia\Exception\Base("Could not load cache frequency file.");
        }
        // if enough time has elapsed, clear the cache
        if ($lastClear + $seconds > time())return false;
        return true;
    }
    protected function deleteStaleCacheFiles(){
        $this->lia->cacheFile('lia.Cache.LastCleanupCheck', time(), 60*60*24*180);
        $dir = $this->lia->get('lia.cacheDir');
        if (!is_dir($dir))throw new \Lia\Exception\Base("The cache directory '{$dir}' does not exist, thus cannot be checked for stale files.");
        
        $dh = opendir($dir);
        $openLen = strlen('{"expiry":');
        $endLen = -1 * strlen('}');
        $now = time();
        while (($f = readdir($dh)) !== false){
            if (substr($f,0,5)!='meta-')continue;
            $content = trim(file_get_contents($dir.'/'.$f));
            $expiresAt = substr($content, $openLen, $endLen);
            if ($expiresAt>$now)continue;
            $actualFile = $dir.'/file-'.substr($f, 5);
            if (is_file($actualFile))unlink($actualFile);
            //delete the meta file
            unlink($dir.'/'.$f);
        }
        closedir($dh);
    }
}