From 38e51166c3fa15c2cdc0da7419dc5d69b4b9276b Mon Sep 17 00:00:00 2001 From: Belle Aerni Date: Fri, 26 May 2023 23:46:42 -0700 Subject: [PATCH] Implemented APCu caching (#44) * Start implementing APCu caching * Fix tests * Now make PHPStan happy :) * Fix tests * Also enable the APCu extension in the tests * Set maxlife to 7 days & clear with cron * Implement a more correct way to clear the cache * Also mention the APCu caching in the readme --- .github/workflows/unittests.yml | 1 + readme.md | 3 +- src/AntCMS/AntCache.php | 120 +++++++++++++++++++++++++------- src/AntCMS/AntConfig.php | 4 +- src/AntCMS/AntTwig.php | 2 +- src/Content/index.md | 2 +- src/cron.php | 15 ++-- tests/ConfigTest.php | 4 +- tests/Includes/Config.yaml | 2 +- tests/MarkdownTest.php | 6 +- 10 files changed, 117 insertions(+), 42 deletions(-) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 24ef6e8..b0248c5 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -23,3 +23,4 @@ jobs: bootstrap: src/Vendor/autoload.php php_version: ${{ matrix.php_version }} args: "tests" + php_extensions: apcu diff --git a/readme.md b/readme.md index 65a22a6..85cbbe5 100644 --- a/readme.md +++ b/readme.md @@ -12,13 +12,14 @@ AntCMS is a lightweight CMS system designed for simplicity, speed, and small siz ### How fast is AntCMS? AntCMS is designed for speed, with a simple backend and caching capabilities that allow it to quickly render and deliver pages to users in milliseconds. This speed is further enhanced by the use of Tailwind CSS in the default theme, which is only 25KB. - Our unit tests also ensure that rendering markdown content takes less than 0.015 seconds, as demonstrated by the following recent results: `Markdown rendering speed with cache: 0.000289 VS without: 0.003414`. ### How does it work? Using AntCMS is simple. First, you need an HTML template with special elements for AntCMS. Then, you can write your content using the popular [markdown](https://www.markdownguide.org/cheat-sheet/) formatting syntax. AntCMS will convert the markdown to HTML, integrate it into the template, and send it to the viewer. This process is already quick, but AntCMS also has caching capabilities that can further improve rendering times. +AntCMS will also automatically leverage the APCu extension for caching, which helps to further improve your website's response time. + ### Theming with AntCMS AntCMS stores its themes in the `/Themes` directory. Each theme consists of a simple page layout template. A theme may also have an `/Assets` folder within its directory, which can be accessed directly from the server. Any files stored outside of this folder will be inaccessible. diff --git a/src/AntCMS/AntCache.php b/src/AntCMS/AntCache.php index aea413c..6ab33ce 100644 --- a/src/AntCMS/AntCache.php +++ b/src/AntCMS/AntCache.php @@ -7,6 +7,44 @@ use Symfony\Component\Yaml\Exception\ParseException; class AntCache { + private int $cacheType = 0; + private string $cacheKeyApcu = ''; + + const noCache = 0; + const fileCache = 1; + const apcuCache = 2; + + /** + * Creates a new cache object, sets the correct caching type. ('auto', 'filesystem', 'apcu', or 'none') + */ + public function __construct() + { + $config = AntConfig::currentConfig(); + $mode = $config['cacheMode'] ?? 'auto'; + switch ($mode) { + case 'none': + $this->cacheType = self::noCache; + break; + case 'auto': + if (extension_loaded('apcu') && apcu_enabled()) { + $this->cacheType = self::apcuCache; + $this->cacheKeyApcu = 'AntCMS_' . hash('md5', __DIR__) . '_'; + } else { + $this->cacheType = self::fileCache; + } + break; + case 'filesystem': + $this->cacheType = self::fileCache; + break; + case 'apcu': + $this->cacheType = self::apcuCache; + $this->cacheKeyApcu = 'AntCMS_' . hash('md5', __DIR__) . '_'; + break; + default: + throw new \Exception("Invalid cache type. Must be 'auto', 'filesystem', 'apcu', or 'none'."); + } + } + /** * Caches a value for a given cache key. * @@ -17,17 +55,17 @@ class AntCache */ public function setCache(string $key, string $content) { - $cachePath = AntCachePath . DIRECTORY_SEPARATOR . "{$key}.cache"; - $config = AntConfig::currentConfig(); - if ($config['enableCache']) { - try { - file_put_contents($cachePath, $content); - return true; - } catch (\Exception) { + switch ($this->cacheType) { + case self::noCache: + return false; + case self::fileCache: + $cachePath = AntCachePath . DIRECTORY_SEPARATOR . "{$key}.cache"; + return file_put_contents($cachePath, $content); + case self::apcuCache: + $apcuKey = $this->cacheKeyApcu . $key; + return apcu_store($apcuKey, $content, 7 * 24 * 60 * 60); // Save it for one week. + default: return false; - } - } else { - return true; } } @@ -40,16 +78,21 @@ class AntCache */ public function getCache(string $key) { - $cachePath = AntCachePath . DIRECTORY_SEPARATOR . "{$key}.cache"; - $config = AntConfig::currentConfig(); - if ($config['enableCache']) { - try { - return file_get_contents($cachePath); - } catch (\Exception) { + switch ($this->cacheType) { + case self::noCache: + return false; + case self::fileCache: + $cachePath = AntCachePath . DIRECTORY_SEPARATOR . "{$key}.cache"; + return file_get_contents($cachePath); + case self::apcuCache: + $apcuKey = $this->cacheKeyApcu . $key; + if (apcu_exists($apcuKey)) { + return apcu_fetch($apcuKey); + } else { + return false; + } + default: return false; - } - } else { - return false; } } @@ -62,12 +105,17 @@ class AntCache */ public function isCached(string $key) { - $config = AntConfig::currentConfig(); - if ($config['enableCache']) { - $cachePath = AntCachePath . DIRECTORY_SEPARATOR . "{$key}.cache"; - return file_exists($cachePath); - } else { - return false; + switch ($this->cacheType) { + case self::noCache: + return false; + case self::fileCache: + $cachePath = AntCachePath . DIRECTORY_SEPARATOR . "{$key}.cache"; + return file_exists($cachePath); + case self::apcuCache: + $apcuKey = $this->cacheKeyApcu . $key; + return apcu_exists($apcuKey); + default: + return false; } } @@ -92,4 +140,26 @@ class AntCache return hash('md4', $content . $salt); } } + + public static function clearCache(): void + { + $di = new \RecursiveDirectoryIterator(AntCachePath, \FilesystemIterator::SKIP_DOTS); + $ri = new \RecursiveIteratorIterator($di, \RecursiveIteratorIterator::CHILD_FIRST); + foreach ($ri as $file) { + $file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath()); + } + + if (extension_loaded('apcu') && apcu_enabled()) { + $prefix = 'AntCMS_' . hash('md5', __DIR__) . '_'; + $cacheInfo = apcu_cache_info(); + $keys = $cacheInfo['cache_list']; + + foreach ($keys as $keyInfo) { + $key = $keyInfo['info']; + if (str_starts_with($key, $prefix)) { + apcu_delete($key); + } + } + } + } } diff --git a/src/AntCMS/AntConfig.php b/src/AntCMS/AntConfig.php index 78a760f..fa05bcd 100644 --- a/src/AntCMS/AntConfig.php +++ b/src/AntCMS/AntConfig.php @@ -11,7 +11,7 @@ class AntConfig 'siteInfo', 'forceHTTPS', 'activeTheme', - 'enableCache', + 'cacheMode', 'debug', 'baseURL', 'embed', @@ -29,7 +29,7 @@ class AntConfig ], 'forceHTTPS' => true, 'activeTheme' => 'Default', - 'enableCache' => true, + 'cacheMode' => 'auto', 'debug' => true, 'baseURL' => $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']), 'embed' => [ diff --git a/src/AntCMS/AntTwig.php b/src/AntCMS/AntTwig.php index 3e31d11..a9b2795 100644 --- a/src/AntCMS/AntTwig.php +++ b/src/AntCMS/AntTwig.php @@ -11,7 +11,7 @@ class AntTwig public function __construct(string $theme = null) { - $twigCache = AntConfig::currentConfig('enableCache') ? AntCachePath : false; + $twigCache = (AntConfig::currentConfig('enableCache') !== 'none') ? AntCachePath : false; $this->theme = $theme ?? AntConfig::currentConfig('activeTheme'); if (!is_dir(antThemePath . '/' . $this->theme)) { diff --git a/src/Content/index.md b/src/Content/index.md index d1d4380..b376acb 100644 --- a/src/Content/index.md +++ b/src/Content/index.md @@ -48,7 +48,7 @@ AntCMS stores its configuration in the human-readable "yaml" file format. The ma - `siteTitle: AntCMS` - This configuration sets the title of your AntCMS website. - `forceHTTPS: true` - Set to 'true' by default, enables HTTPs redirection. - `activeTheme: Default` - Sets what theme AntCMS should use. should match the folder name of the theme you want to use. -- `enableCache: true` - Enables or disables file caching in AntCMS. +- `cacheMode: auto` - Allows AntCMS to auto-detect if it should use APCu or the file system for it's cache. Also accepts 'none', 'apcu', and 'filesystem'. - `debug: true`- Enabled or disables debug mode. - `baseURL: antcms.example.com/` - Used to set the baseURL for your AntCMS instance, without the protocol. This will be automatically generated for you, but can be changed if needed. diff --git a/src/cron.php b/src/cron.php index 394f256..36173e1 100644 --- a/src/cron.php +++ b/src/cron.php @@ -1,7 +1,10 @@ isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath()); -} + +require_once __DIR__ . DIRECTORY_SEPARATOR . 'Vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; +$classMapPath = __DIR__ . DIRECTORY_SEPARATOR . 'Cache' . DIRECTORY_SEPARATOR . 'classMap.php'; +$loader = new AntCMS\AntLoader($classMapPath); +$loader->addPrefix('AntCMS\\', __DIR__ . DIRECTORY_SEPARATOR . 'AntCMS'); +$loader->checkClassMap(); +$loader->register(); + +AntCMS\AntCache::clearCache(); diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index 57732c3..c3405a6 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -15,7 +15,7 @@ class ConfigTest extends TestCase 'siteInfo', 'forceHTTPS', 'activeTheme', - 'enableCache', + 'cacheMode', 'debug', 'baseURL' ); @@ -28,7 +28,7 @@ class ConfigTest extends TestCase public function testSaveConfigFailed() { $Badconfig = [ - 'enableCache' => true, + 'cacheMode' => 'none', ]; try { diff --git a/tests/Includes/Config.yaml b/tests/Includes/Config.yaml index af29083..b507398 100644 --- a/tests/Includes/Config.yaml +++ b/tests/Includes/Config.yaml @@ -2,7 +2,7 @@ siteInfo: siteTitle: 'AntCMS' forceHTTPS: true activeTheme: Default -enableCache: true +cacheMode: auto debug: true baseURL: antcms.org/ embed: diff --git a/tests/MarkdownTest.php b/tests/MarkdownTest.php index c2dfc0a..d688462 100644 --- a/tests/MarkdownTest.php +++ b/tests/MarkdownTest.php @@ -22,7 +22,7 @@ class MarkdownTest extends TestCase $currentConfig = AntConfig::currentConfig(); //Ensure cache is enabled - $currentConfig['enableCache'] = true; + $currentConfig['cacheMode'] = 'auto'; AntConfig::saveConfig($currentConfig); for ($i = 0; $i < 10; ++$i) { @@ -48,7 +48,7 @@ class MarkdownTest extends TestCase $currentConfig = AntConfig::currentConfig(); //Disable cache - $currentConfig['enableCache'] = false; + $currentConfig['cacheMode'] = 'none'; AntConfig::saveConfig($currentConfig); $totalTime = 0; @@ -62,7 +62,7 @@ class MarkdownTest extends TestCase $withoutCache = $totalTime / 10; //Enable cache - $currentConfig['enableCache'] = true; + $currentConfig['cacheMode'] = 'auto'; AntConfig::saveConfig($currentConfig); $totalTime = 0;