diff --git a/.github/workflows/php83.yml b/.github/workflows/php83.yml index 20b2f47e2..24b9d47c7 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 cf553cdec..0ed2e9860 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 000000000..f65ef6645 --- /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/tests/webfiori/framework/test/cli/HelpCommandTest.php b/tests/webfiori/framework/test/cli/HelpCommandTest.php new file mode 100644 index 000000000..9d516645a --- /dev/null +++ b/tests/webfiori/framework/test/cli/HelpCommandTest.php @@ -0,0 +1,39 @@ +assertEquals([ + "WebFiori Framework (c) Version ". WF_VERSION." ".WF_VERSION_TYPE."\n\n\n", + "Usage:\n", + " command [arg1 arg2=\"val\" arg3...]\n\n", + "Global Arguments:\n", + " --ansi:[Optional] Force the use of ANSI output.\n", + "Available Commands:\n", + " help: Display CLI Help. To display help for specific command, use the argument \"--command-name\" with this command.\n", + " v: Display framework version info.\n", + " show-settings: Display application configuration.\n", + " scheduler: Run tasks scheduler.\n", + " create: Creates a system entity (middleware, web service, background process ...).\n", + " add: Add a database connection or SMTP account.\n", + " list-routes: List all created routes and which resource they point to.\n", + " list-themes: List all registered themes.\n", + " run-query: Execute SQL query on specific database.\n", + " update-settings: Update application settings which are stored in specific configuration driver.\n", + " update-table: Update a database table.\n", + ], $this->executeMultiCommand([ + 'help', + ])); + $this->assertEquals(0, $this->getExitCode()); + } +} diff --git a/webfiori/framework/App.php b/webfiori/framework/App.php index 691ebddda..4c41b4a15 100644 --- a/webfiori/framework/App.php +++ b/webfiori/framework/App.php @@ -18,6 +18,7 @@ use webfiori\file\exceptions\FileException; use webfiori\file\File; use webfiori\framework\autoload\ClassLoader; +use webfiori\framework\cache\Cache; use webfiori\framework\config\ConfigurationDriver; use webfiori\framework\config\Controller; use webfiori\framework\exceptions\InitializationException; @@ -179,11 +180,21 @@ private function __construct() { foreach ($uriObj->getMiddleware() as $mw) { $mw->after(Request::get(), Response::get()); } + App::cacheResponse($uriObj->getUri(true, true), $uriObj->getCacheDuration()); } }); //class is now initialized self::$ClassStatus = self::STATUS_INITIALIZED; } + public static function cacheResponse(string $key, int $duration) { + Cache::get($key, function () { + return [ + 'headers' => Response::getHeaders(), + 'http-code' => Response::getCode(), + 'body' => Response::getBody() + ]; + }, $duration); + } /** * Register CLI commands or background tasks. * diff --git a/webfiori/framework/cache/Cache.php b/webfiori/framework/cache/Cache.php new file mode 100644 index 000000000..f5d1bf861 --- /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 000000000..500a39ee5 --- /dev/null +++ b/webfiori/framework/cache/FileStorage.php @@ -0,0 +1,160 @@ +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) { + if ($item->getTTL() > 0) { + $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 000000000..c8c206671 --- /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 mixed + */ + public function getDataDecrypted() { + return unserialize($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(serialize($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 000000000..e9a870632 --- /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); +} diff --git a/webfiori/framework/cli/commands/UpdateSettingsCommand.php b/webfiori/framework/cli/commands/UpdateSettingsCommand.php index be850b020..b1345151d 100644 --- a/webfiori/framework/cli/commands/UpdateSettingsCommand.php +++ b/webfiori/framework/cli/commands/UpdateSettingsCommand.php @@ -33,7 +33,7 @@ public function __construct() { .'Possible values are: version, app-name, scheduler-pass, page-title, ' .'page-description, primary-lang, title-sep, home-page, theme,' .'admin-theme.', true), - ], 'Update application settings which are stored in the class "AppConfig".'); + ], 'Update application settings which are stored in specific configuration driver.'); } public function exec() : int { $options = []; diff --git a/webfiori/framework/router/RouteOption.php b/webfiori/framework/router/RouteOption.php index 0a3827d74..4bcbd3835 100644 --- a/webfiori/framework/router/RouteOption.php +++ b/webfiori/framework/router/RouteOption.php @@ -28,6 +28,10 @@ class RouteOption { * An option which is used to indicate if path is case sensitive or not. */ const CASE_SENSITIVE = 'case-sensitive'; + /** + * An option which is used to set the duration of route cache in seconds. + */ + const CACHE_DURATION = 'cache-ttl'; /** * An option which is used to set an array as closure parameters (applies to routes of type closure only) */ diff --git a/webfiori/framework/router/Router.php b/webfiori/framework/router/Router.php index b8f6a4350..a3860acab 100644 --- a/webfiori/framework/router/Router.php +++ b/webfiori/framework/router/Router.php @@ -15,6 +15,7 @@ use webfiori\cli\Runner; use webfiori\file\exceptions\FileException; use webfiori\file\File; +use webfiori\framework\cache\Cache; use webfiori\framework\exceptions\RoutingException; use webfiori\framework\ui\HTTPCodeView; use webfiori\framework\ui\StarterPage; @@ -507,14 +508,16 @@ public static function incSiteMapRoute() { Response::addHeader('content-type','text/xml'); }; self::closure([ - 'path' => '/sitemap.xml', - 'route-to' => $sitemapFunc, - 'in-sitemap' => true + RouteOption::PATH => '/sitemap.xml', + RouteOption::TO => $sitemapFunc, + RouteOption::SITEMAP => true, + RouteOption::CACHE_DURATION => 86400//1 day ]); self::closure([ - 'path' => '/sitemap', - 'route-to' => $sitemapFunc, - 'in-sitemap' => true + RouteOption::PATH => '/sitemap', + RouteOption::TO => $sitemapFunc, + RouteOption::SITEMAP => true, + RouteOption::CACHE_DURATION => 86400//1 day ]); } /** @@ -529,7 +532,8 @@ public static function notFound() { * Adds new route to a web page. * * Note that the route which created using this method will be added to - * 'global' and 'web' middleware groups. + * 'global' and 'web' middleware groups. Additionally, the routes will + * be cached for one hour. * * @param array $options An associative array that contains route * options. Available options are: @@ -755,13 +759,14 @@ private function addRouteHelper0($options): bool { $asApi = $options[RouteOption::API]; $closureParams = $options[RouteOption::CLOSURE_PARAMS] ; $path = $options[RouteOption::PATH]; + $cache = $options[RouteOption::CACHE_DURATION]; if ($routeType == self::CLOSURE_ROUTE && !is_callable($routeTo)) { return false; } $routeUri = new RouterUri($this->getBase().$path, $routeTo,$caseSensitive, $closureParams); $routeUri->setAction($options[RouteOption::ACTION]); - + $routeUri->setCacheDuration($cache); if (!$this->hasRouteHelper($routeUri)) { if ($asApi === true) { $routeUri->setType(self::API_ROUTE); @@ -928,6 +933,12 @@ private function checkOptionsArr(array $options): array { } else { $caseSensitive = true; } + + if (isset($options[RouteOption::CACHE_DURATION])) { + $cacheDuration = $options[RouteOption::CACHE_DURATION]; + } else { + $cacheDuration = 0; + } $routeType = $options[RouteOption::TYPE] ?? Router::CUSTOMIZED; @@ -978,7 +989,8 @@ private function checkOptionsArr(array $options): array { RouteOption::VALUES => $varValues, RouteOption::MIDDLEWARE => $mdArr, RouteOption::REQUEST_METHODS => $this->getRequestMethodsHelper($options), - RouteOption::ACTION => $action + RouteOption::ACTION => $action, + RouteOption::CACHE_DURATION => $cacheDuration ]; } private function copyOptionsToSub($options, &$subRoute) { @@ -1376,7 +1388,6 @@ private function routeFound(RouterUri $route, bool $loadResource) { if ($route->getType() == self::API_ROUTE && !defined('API_CALL')) { define('API_CALL', true); } - if (is_callable($route->getRouteTo())) { if ($loadResource === true) { call_user_func_array($route->getRouteTo(),$route->getClosureParams()); @@ -1453,6 +1464,16 @@ private function routeFound(RouterUri $route, bool $loadResource) { * @throws RoutingException */ private function searchRoute(RouterUri $routeUri, string $uri, bool $loadResource, bool $withVars = false): bool { + $data = Cache::get($uri); + + if ($data !== null) { + Response::write($data['body']); + Response::setCode($data['http-code']); + foreach ($data['headers'] as $headerObj) { + Response::addHeader($headerObj->getName(), $headerObj->getValue()); + } + return true; + } $pathArray = $routeUri->getPathArray(); $requestMethod = Request::getMethod(); $indexToSearch = 'static'; @@ -1600,7 +1621,10 @@ private static function view(array $options): bool { if (gettype($options) == 'array') { $options[RouteOption::TYPE] = Router::VIEW_ROUTE; self::addToMiddlewareGroup($options, 'web'); - + if (!isset($options[RouteOption::CACHE_DURATION])) { + //Cache pages for 1 hour by default + $options[RouteOption::CACHE_DURATION] = 3600; + } return Router::getInstance()->addRouteHelper1($options); } diff --git a/webfiori/framework/router/RouterUri.php b/webfiori/framework/router/RouterUri.php index d96e4a42b..d2d4305ab 100644 --- a/webfiori/framework/router/RouterUri.php +++ b/webfiori/framework/router/RouterUri.php @@ -46,6 +46,7 @@ class RouterUri extends Uri { */ private $action; private $assignedMiddlewareList; + private $cacheDuration; /** * * @var array @@ -137,6 +138,27 @@ public function __construct(string $requestedUri, $routeTo, bool $caseSensitive $this->incInSiteMap = false; $this->languages = []; $this->addMiddleware('global'); + $this->setCacheDuration(0); + } + /** + * Returns the duration of URI cache. + * + * @return int The duration of URI cache. Default value is zero which indicates + * that no caching will happen. + */ + public function getCacheDuration() : int { + return $this->cacheDuration; + } + /** + * Sets the duration of URI cache. + * + * @param int $val A positive value that represent cache duration in seconds. + * If 0 is given, it indicates that no caching will happen. + */ + public function setCacheDuration(int $val) { + if ($val >= 0) { + $this->cacheDuration = $val; + } } /** * Adds a language to the set of languages at which the resource that the URI