knot-lib/cache

View on GitHub
src/FileCache.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php /** @noinspection PhpMissingReturnTypeInspection */
/** @noinspection DuplicatedCode */
declare(strict_types=1);

namespace knotlib\cache;

use DateInterval;
use Psr\SimpleCache\InvalidArgumentException;

use Stk2k\FileSystem\File;
use Stk2k\FileSystem\Exception\FileOutputException;
use Stk2k\FileSystem\Exception\FileInputException;
use knotlib\cache\config\FileCacheConfig;
use knotlib\cache\util\CacheDataTrait;
use knotlib\cache\exception\CacheFileNotReadableException;
use knotlib\cache\exception\CacheFileFormatException;
use knotlib\cache\exception\CacheMetaDataException;

class FileCache implements CacheInterface
{
    use CacheDataTrait;

    const CACHE_FILE_EXT  = '.cache';

    /** @var FileCacheConfig */
    private $config;

    /**
     * FileCacheDriver constructor.
     *
     * @param array $config
     */
    public function __construct(array $config = [])
    {
        $this->config = new FileCacheConfig($config);
    }

    /**
     * {@inheritDoc}
     *
     * @throws CacheFileNotReadableException
     * @throws FileInputException
     * @throws CacheFileFormatException
     * @throws CacheMetaDataException
     */
    public function get($key, $default = null)
    {
        $cache_file = $this->getCacheItemFile($key);

        if (!$cache_file->exists()){
            return $default;
        }
        if (!$cache_file->canRead()){
            throw new CacheFileNotReadableException($cache_file);
        }

        $cache_info = json_decode($cache_file->get(), true);

        if (!is_array($cache_info)){
            throw new CacheFileFormatException($cache_file);
        }

        // check expire date
        $expires = $cache_info['expires'] ?? null;
        if ($expires === null){
            throw( new CacheMetaDataException('expires not found') );
        }
        $now = time();
        if ($expires > 0 && $expires < $now){
            return $default;
        }

        // decode data
        $data = $cache_info['data'] ?? null;
        if ($data === null){
            throw( new CacheMetaDataException('data not found') );
        }
        $data = $this->decodeData($data);

        // type check
        $type = $cache_info['type'] ?? null;
        if ($type === null){
            throw( new CacheMetaDataException('type not found') );
        }
        if ($type !== gettype($data)){
            return $default;
        }

        // class check
        if ($type === 'object'){
            $class = $cache_info['class'] ?? null;
            if ($class === null){
                throw( new CacheMetaDataException('class not found') );
            }
            if (!($data instanceof $class)){
                return $default;
            }
        }

        return $data;
    }

    /**
     * {@inheritDoc}
     *
     * @param string $key
     * @param mixed $value
     * @param null $ttl
     *
     * @return bool
     * @throws FileInputException
     * @throws FileOutputException
     */
    public function set($key, $value, $ttl = null)
    {
        $cache_file = $this->getCacheItemFile($key);

        if ($ttl instanceof DateInterval){
            $ttl = intval($ttl->format('s'));
        }

        $expire = $ttl ?? $this->config->getExpire();

        // If a negative or zero TTL is provided, the item MUST be deleted from the cache if it exists, as it is expired already.
        if ($expire < 0){
            $cache_file = $this->getCacheItemFile($key);
            if ($cache_file->exists()){
                $cache_file->delete();
            }
            return true;
        }

        $expires = $expire > 0 ? time() + $expire : 0;

        $content['expires'] = $expires;
        $content['type'] = gettype($value);
        $content['class'] = is_object($value) ? get_class($value) : '';
        $content['data'] = $this->encodeData($value);

        $cache_file->put(json_encode($content));

        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function delete($key)
    {
        $cache_file = $this->getCacheItemFile($key);

        if ($cache_file->exists()) {
            return $cache_file->delete();
        }

        return true;
    }

    /**
     * Rewrite cache expiration time
     *
     * @param string $key         The key of the item to remove. Shell wildcards are accepted.
     * @param int $duration       specify expiration span which the cache will be removed.
     *
     * @return bool
     *
     * @throws FileOutputException
     * @throws CacheFileFormatException
     * @throws FileInputException
     */
    public function touch( $key, $duration = NULL )
    {
        $cache_file = $this->getCacheItemFile($key);

        if (!$cache_file->exists()){
            return true;
        }

        $cache_info = json_decode($cache_file->get(), true);

        if (!is_array($cache_info)){
            throw new CacheFileFormatException($cache_file);
        }

        $expire = $this->config->getExpire();
        $expires = $expire > 0 ? time() + $expire : 0;

        $content['expires'] = $expires;
        $content['type']    = $cache_info['type'];
        $content['class']   = $cache_info['class'];
        $content['data']    = $cache_info['data'];

        $cache_file->put(json_encode($content));

        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function clear()
    {
        $root = $this->config->getCacheRoot();
        $search = $root . '/*' . self::CACHE_FILE_EXT;
        $res = true;
        foreach(glob($search) as $file){
            $res &= @unlink($file);
        }
        return $res;
    }

    /**
     * {@inheritDoc}
     *
     * @throws CacheFileNotReadableException
     * @throws FileInputException
     * @throws CacheFileFormatException
     * @throws CacheMetaDataException
     */
    public function getMultiple($keys, $default = null)
    {
        $ret = [];
        foreach($keys as $key){
            $ret[$key] = $this->get($key, $default);
        }
        return $ret;
    }

    /**
     * {@inheritDoc}
     *
     * @param iterable $values
     * @param null $ttl
     *
     * @return bool
     * @throws FileInputException
     * @throws FileOutputException
     * @throws InvalidArgumentException
     */
    public function setMultiple($values, $ttl = null)
    {
        $ret = true;
        foreach($values as $key => $value){
            $ret &= $this->set($key, $value, $ttl);
        }
        return $ret;
    }

    /**
     * {@inheritDoc}
     */
    public function deleteMultiple($keys)
    {
        $ret = true;
        foreach($keys as $key){
            $ret &= $this->delete($key);
        }
        return $ret;
    }

    /**
     * {@inheritDoc}
     */
    public function has($key)
    {
        return $this->getCacheItemFile($key)->exists();
    }

    /**
     * @param $key
     *
     * @return File
     */
    private function getCacheItemFile($key) : File
    {
        $cache_root = new File($this->config->getCacheRoot());
        return new File($key.self::CACHE_FILE_EXT, $cache_root);
    }
}