diff --git a/.github/workflows/php83.yml b/.github/workflows/php83.yml
index 20b2f47e..24b9d47c 100644
--- a/.github/workflows/php83.yml
+++ b/.github/workflows/php83.yml
@@ -4,7 +4,7 @@ on:
push:
branches: [ master, dev ]
pull_request:
- branches: [ master ]
+ branches: [ master, dev ]
jobs:
diff --git a/phpunit.xml b/phpunit.xml
index cf553cde..0ed2e986 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -28,6 +28,10 @@
./webfiori/framework/router/RouterUri.php
./webfiori/framework/router/Router.php
+ ./webfiori/framework/cache/AbstractCacheStore.php
+ ./webfiori/framework/cache/FileCacheStore.php
+ ./webfiori/framework/cache/Cache.php
+
./webfiori/framework/session/Session.php
./webfiori/framework/session/SessionsManager.php
./webfiori/framework/session/DefaultSessionStorage.php
@@ -109,6 +113,8 @@
./tests/webfiori/framework/test/session
-
+
+ ./tests/webfiori/framework/test/cache
+
\ No newline at end of file
diff --git a/tests/webfiori/framework/test/cache/CacheTest.php b/tests/webfiori/framework/test/cache/CacheTest.php
new file mode 100644
index 00000000..f65ef664
--- /dev/null
+++ b/tests/webfiori/framework/test/cache/CacheTest.php
@@ -0,0 +1,87 @@
+assertEquals('This is a test.', $data);
+ $this->assertEquals('This is a test.', Cache::get($key));
+ $this->assertNull(Cache::get('not_cached'));
+ }
+ /**
+ * @test
+ */
+ public function test01() {
+ $key = 'test_2';
+ $this->assertFalse(Cache::has($key));
+ $data = Cache::get($key, function () {
+ return 'This is a test.';
+ }, 5);
+ $this->assertEquals('This is a test.', $data);
+ $this->assertTrue(Cache::has($key));
+ sleep(6);
+ $this->assertFalse(Cache::has($key));
+ $this->assertNull(Cache::get($key));
+ }
+ /**
+ * @test
+ */
+ public function test03() {
+ $key = 'ok_test';
+ $this->assertFalse(Cache::has($key));
+ $data = Cache::get($key, function () {
+ return 'This is a test.';
+ }, 600);
+ $this->assertEquals('This is a test.', $data);
+ $this->assertTrue(Cache::has($key));
+ Cache::delete($key);
+ $this->assertFalse(Cache::has($key));
+ $this->assertNull(Cache::get($key));
+ }
+ /**
+ * @test
+ */
+ public function test04() {
+ $key = 'test_3';
+ $this->assertFalse(Cache::has($key));
+ $data = Cache::get($key, function () {
+ return 'This is a test.';
+ }, 600);
+ $this->assertEquals('This is a test.', $data);
+ $item = Cache::getItem($key);
+ $this->assertNotNull($item);
+ $this->assertEquals(600, $item->getTTL());
+ Cache::setTTL($key, 1000);
+ $item = Cache::getItem($key);
+ $this->assertEquals(1000, $item->getTTL());
+ Cache::delete($key);
+ $this->assertNull(Cache::getItem($key));
+ }
+ public function test05() {
+ $keys = [];
+ for ($x = 0 ; $x < 10 ; $x++) {
+ $key = 'item_'.$x;
+ Cache::get($key, function () {
+ return 'This is a test.';
+ }, 600);
+ $keys[] = $key;
+ }
+ foreach ($keys as $key) {
+ $this->assertTrue(Cache::has($key));
+ }
+ Cache::flush();
+ foreach ($keys as $key) {
+ $this->assertFalse(Cache::has($key));
+ }
+ }
+}
diff --git a/webfiori/framework/cache/Cache.php b/webfiori/framework/cache/Cache.php
new file mode 100644
index 00000000..f5d1bf86
--- /dev/null
+++ b/webfiori/framework/cache/Cache.php
@@ -0,0 +1,172 @@
+delete($key);
+ }
+ /**
+ * Removes all items from the cache.
+ */
+ public static function flush() {
+ self::getDriver()->flush();
+ }
+ /**
+ * Returns or creates a cache item given its key.
+ *
+ *
+ * @param string $key The unique identifier of the item.
+ *
+ * @param callable $generator A callback which is used as a fallback to
+ * create new cache entry or re-create an existing one if it was expired.
+ * This callback must return the data that will be cached.
+ *
+ * @param int $ttl Time to live of the item in seconds.
+ *
+ * @param array $params Any additional parameters to be passed to the callback
+ * which is used to generate cache data.
+ * @return null
+ */
+ public static function get(string $key, callable $generator = null, int $ttl = 60, array $params = []) {
+ $data = self::getDriver()->read($key);
+
+ if ($data !== null && $data !== false) {
+ return $data;
+ }
+
+ if (!is_callable($generator)) {
+ return null;
+ }
+ $newData = call_user_func_array($generator, $params);
+ $item = new Item($key, $newData, $ttl, defined('CACHE_SECRET') ? CACHE_SECRET : '');
+ self::getDriver()->cache($item);
+
+ return $newData;
+ }
+ /**
+ * Returns storage engine which is used to store, read, update and delete items
+ * from the cache.
+ *
+ * @return Storage
+ */
+ public static function getDriver() : Storage {
+ return self::getInst()->driver;
+ }
+ /**
+ * Reads an item from the cache and return its information.
+ *
+ * @param string $key The unique identifier of the item.
+ *
+ * @return Item|null If such item exist and not yet expired, an object
+ * of type 'Item' is returned which has all cached item information. Other
+ * than that, null is returned.
+ */
+ public static function getItem(string $key) {
+ return self::getDriver()->readItem($key);
+ }
+ /**
+ * Checks if the cache has in item given its unique identifier.
+ *
+ * @param string $key
+ *
+ * @return bool If the item exist and is not yet expired, true is returned.
+ * Other than that, false is returned.
+ */
+ public static function has(string $key) : bool {
+ return self::getDriver()->has($key);
+ }
+ /**
+ * Creates new item in the cache.
+ *
+ * Note that the item will only be added if it does not exist or already
+ * expired or the override option is set to true in case it was already
+ * created and not expired.
+ *
+ * @param string $key The unique identifier of the item.
+ *
+ * @param mixed $data The data that will be cached.
+ *
+ * @param int $ttl The time at which the data will be kept in the cache (in seconds).
+ *
+ * @param bool $override If cache item already exist which has given key and not yet
+ * expired and this one is set to true, the existing item will be overridden by
+ * provided data and ttl.
+ *
+ * @return bool If successfully added, the method will return true. False
+ * otherwise.
+ */
+ public static function set(string $key, $data, int $ttl = 60, bool $override = false) : bool {
+ if (!self::has($key) || $override === true) {
+ $item = new Item($key, $data, $ttl, defined('CACHE_SECRET') ? CACHE_SECRET : '');
+ self::getDriver()->cache($item);
+ }
+
+ return false;
+ }
+ /**
+ * Sets storage engine which is used to store, read, update and delete items
+ * from the cache.
+ *
+ * @param Storage $driver
+ */
+ public static function setDriver(Storage $driver) {
+ self::getInst()->driver = $driver;
+ }
+ /**
+ * Updates TTL of specific cache item.
+ *
+ * @param string $key The unique identifier of the item.
+ *
+ * @param int $ttl The new value for TTL.
+ *
+ * @return bool If item is updated, true is returned. Other than that, false
+ * is returned.
+ */
+ public static function setTTL(string $key, int $ttl) {
+ $item = self::getItem($key);
+
+ if ($item === null) {
+ return false;
+ }
+ $item->setTTL($ttl);
+ self::getDriver()->cache($item);
+
+ return true;
+ }
+ /**
+ * Creates and returns a single instance of the class.
+ *
+ * @return Cache
+ */
+ private static function getInst() : Cache {
+ if (self::$inst === null) {
+ self::$inst = new Cache();
+ self::setDriver(new FileStorage());
+ }
+
+ return self::$inst;
+ }
+}
diff --git a/webfiori/framework/cache/FileStorage.php b/webfiori/framework/cache/FileStorage.php
new file mode 100644
index 00000000..17b40b5f
--- /dev/null
+++ b/webfiori/framework/cache/FileStorage.php
@@ -0,0 +1,158 @@
+setPath($path);
+ }
+ /**
+ * Store an item into the cache.
+ *
+ * @param Item $item An item that will be added to the cache.
+ */
+ public function cache(Item $item) {
+ $filePath = $this->getPath().DS.md5($item->getKey()).'.cache';
+ $encryptedData = $item->getDataEncrypted();
+
+ if (!is_dir($this->getPath())) {
+ mkdir($this->getPath(), 0755, true);
+ }
+ file_put_contents($filePath, serialize([
+ 'data' => $encryptedData,
+ 'created_at' => time(),
+ 'ttl' => $item->getTTL(),
+ 'expires' => $item->getExpiryTime(),
+ 'key' => $item->getKey()
+ ]));
+ }
+ /**
+ * Removes an item from the cache.
+ *
+ * @param string $key The key of the item.
+ */
+ public function delete(string $key) {
+ $filePath = $this->getPath().md5($key).'.cache';
+
+ if (file_exists($filePath)) {
+ unlink($filePath);
+ }
+ }
+ /**
+ * Removes all cached items.
+ *
+ */
+ public function flush() {
+ $files = glob($this->cacheDir.'*.cache');
+
+ foreach ($files as $file) {
+ unlink($file);
+ }
+ }
+ /**
+ * Returns a string that represents the path to the folder which is used to
+ * create cache files.
+ *
+ * @return string A string that represents the path to the folder which is used to
+ * create cache files.
+ */
+ public function getPath() : string {
+ return $this->cacheDir;
+ }
+ /**
+ * Checks if an item exist in the cache.
+ * @param string $key The value of item key.
+ *
+ * @return bool Returns true if given
+ * key exist in the cache and not yet expired.
+ */
+ public function has(string $key): bool {
+ return $this->read($key) !== null;
+ }
+ /**
+ * Reads and returns the data stored in cache item given its key.
+ *
+ * @param string $key The key of the item.
+ *
+ * @return mixed|null If cache item is not expired, its data is returned. Other than
+ * that, null is returned.
+ */
+ public function read(string $key) {
+ $item = $this->readItem($key);
+
+ if ($item !== null) {
+ return $item->getDataDecrypted();
+ }
+
+ return null;
+ }
+ /**
+ * Reads cache item as an object given its key.
+ *
+ * @param string $key The unique identifier of the item.
+ *
+ * @return Item|null If cache item exist and is not expired,
+ * an object of type 'Item' is returned. Other than
+ * that, null is returned.
+ */
+ public function readItem(string $key) {
+ $this->initData($key);
+ $now = time();
+
+ if ($now > $this->data['expires']) {
+ $this->delete($key);
+
+ return null;
+ }
+ $item = new Item($key, $this->data['data'], $this->data['ttl'], defined('CACHE_SECRET') ? CACHE_SECRET : '');
+ $item->setCreatedAt($this->data['created_at']);
+
+ return $item;
+ }
+ /**
+ * Sets the path to the folder which is used to create cache files.
+ *
+ * @param string $path
+ */
+ public function setPath(string $path) {
+ $this->cacheDir = $path;
+ }
+ private function initData(string $key) {
+ $filePath = $this->cacheDir.md5($key).'.cache';
+
+ if (!file_exists($filePath)) {
+ $this->data = [
+ 'expires' => 0,
+ 'ttl' => 0,
+ 'data' => null,
+ 'created_at' => 0,
+ 'key' => ''
+ ];
+
+ return ;
+ }
+
+ $this->data = unserialize(file_get_contents($filePath));
+ }
+}
diff --git a/webfiori/framework/cache/Item.php b/webfiori/framework/cache/Item.php
new file mode 100644
index 00000000..c4a88789
--- /dev/null
+++ b/webfiori/framework/cache/Item.php
@@ -0,0 +1,196 @@
+setKey($key);
+ $this->setTTL($ttl);
+ $this->setData($data);
+ $this->setSecret($secretKey);
+ $this->setCreatedAt(time());
+ }
+ /**
+ * Generates a cryptographic secure key.
+ *
+ * The generated key can be used to encrypt sensitive data.
+ *
+ * @return string
+ */
+ public static function generateKey() : string {
+ return bin2hex(random_bytes(32));
+ }
+ /**
+ * Returns the time at which the item was created at.
+ *
+ * The value returned by the method is Unix timestamp.
+ *
+ * @return int An integer that represents Unix timestamp in seconds.
+ */
+ public function getCreatedAt() : int {
+ return $this->createdAt;
+ }
+ /**
+ * Returns the data of cache item.
+ *
+ * @return mixed
+ */
+ public function getData() {
+ return $this->data;
+ }
+ /**
+ * Returns cache item data after performing decryption on it.
+ *
+ * @return string
+ */
+ public function getDataDecrypted() : string {
+ return $this->decrypt($this->getData());
+ }
+ /**
+ * Returns cache data after performing encryption on it.
+ *
+ * Note that the raw data must be
+ *
+ * @return string
+ */
+ public function getDataEncrypted() : string {
+ return $this->encrypt($this->getData());
+ }
+ /**
+ * Returns the time at which cache item will expire as Unix timestamp.
+ *
+ * The method will add the time at which the item was created at to TTL and
+ * return the value.
+ *
+ * @return int The time at which cache item will expire as Unix timestamp.
+ */
+ public function getExpiryTime() : int {
+ return $this->getCreatedAt() + $this->getTTL();
+ }
+ /**
+ * Gets the key of the item.
+ *
+ * The key acts as a unique identifier for cache items.
+ *
+ * @return string A string that represents the key.
+ */
+ public function getKey() : string {
+ return $this->key;
+ }
+ /**
+ * Returns the value of the key which is used in encrypting cache data.
+ *
+ * @return string The value of the key which is used in encrypting cache data.
+ * Default return value is empty string.
+ */
+ public function getSecret() : string {
+ return $this->secretKey;
+ }
+ /**
+ * Returns the duration at which the item will be kept in cache in seconds.
+ *
+ * @return int The duration at which the item will be kept in cache in seconds.
+ */
+ public function getTTL() : int {
+ return $this->timeToLive;
+ }
+ /**
+ * Sets the time at which the item was created at.
+ *
+ * @param int $time An integer that represents Unix timestamp in seconds.
+ * Must be a positive value.
+ */
+ public function setCreatedAt(int $time) {
+ if ($time > 0) {
+ $this->createdAt = $time;
+ }
+ }
+ /**
+ * Sets the data of the item.
+ *
+ * This represents the data that will be stored or retrieved.
+ *
+ * @param mixed $data
+ */
+ public function setData($data) {
+ $this->data = $data;
+ }
+ /**
+ * Sets the key of the item.
+ *
+ * The key acts as a unique identifier for cache items.
+ *
+ * @param string $key A string that represents the key.
+ */
+ public function setKey(string $key) {
+ $this->key = $key;
+ }
+ /**
+ * Sets the value of the key which is used in encrypting cache data.
+ *
+ * @param string $secret A cryptographic key which is used to encrypt
+ * cache data. To generate one, the method Item::generateKey() can be used.
+ */
+ public function setSecret(string $secret) {
+ $this->secretKey = $secret;
+ }
+ /**
+ * Sets the duration at which the item will be kept in cache in seconds.
+ *
+ * @param int $ttl Time-to-live of the item in cache.
+ */
+ public function setTTL(int $ttl) {
+ if ($ttl > 0) {
+ $this->timeToLive = $ttl;
+ }
+ }
+
+
+ private function decrypt($data) {
+ // decode > extract iv > decrypt
+ $decodedData = base64_decode($data);
+ $ivLength = openssl_cipher_iv_length('aes-256-cbc');
+ $iv = substr($decodedData, 0, $ivLength);
+ $encryptedData = substr($decodedData, $ivLength);
+ $decrypted = openssl_decrypt($encryptedData, 'aes-256-cbc', $this->getSecret(), 0, $iv);
+
+ return $decrypted;
+ }
+ private function encrypt($data) {
+ // iv > encrypt > append iv > encode
+ $iv = random_bytes(openssl_cipher_iv_length('aes-256-cbc'));
+ $encryptedData = openssl_encrypt($data, 'aes-256-cbc', $this->getSecret(), 0, $iv);
+ $encoded = base64_encode($iv.$encryptedData);
+
+ return $encoded;
+ }
+}
diff --git a/webfiori/framework/cache/Storage.php b/webfiori/framework/cache/Storage.php
new file mode 100644
index 00000000..e9a87063
--- /dev/null
+++ b/webfiori/framework/cache/Storage.php
@@ -0,0 +1,81 @@
+
+ *
key
+ * data
+ * time to live
+ * creation time
+ *
+ *
+ * @param Item $item An item that will be added to the cache.
+ */
+ public function cache(Item $item);
+ /**
+ * Removes an item from the cache.
+ *
+ * @param string $key The key of the item.
+ */
+ public function delete(string $key);
+ /**
+ * Removes all cached items.
+ *
+ * This method must be implemented in a way that it removes all cache items
+ * regardless of expiry time.
+ */
+ public function flush();
+ /**
+ * Checks if an item exist in the cache.
+ *
+ * This method must be implemented in a way that it returns true if given
+ * key exist in the cache and not yet expired.
+ *
+ * @param string $key The value of item key.
+ *
+ * @return bool Returns true if given
+ * key exist in the cache and not yet expired.
+ */
+ public function has(string $key) : bool;
+ /**
+ * Reads and returns the data stored in cache item given its key.
+ *
+ * This method should be implemented in a way that it reads cache item
+ * as an object of type 'Item'. Then it should do a check if the cached
+ * item is expired or not. If not expired, its data is returned. Other than
+ * that, null should be returned.
+ *
+ * @param string $key The key of the item.
+ *
+ * @return mixed|null If cache item is not expired, its data is returned. Other than
+ * that, null is returned.
+ */
+ public function read(string $key);
+ /**
+ * Reads cache item as an object given its key.
+ *
+ * @param string $key The unique identifier of the item.
+ *
+ * @return Item|null If cache item exist and is not expired,
+ * an object of type 'Item' should be returned. Other than
+ * that, null is returned.
+ */
+ public function readItem(string $key);
+}