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
This commit is contained in:
Belle Aerni 2023-05-26 23:46:42 -07:00 committed by GitHub
parent b61ac80227
commit 38e51166c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 117 additions and 42 deletions

View file

@ -23,3 +23,4 @@ jobs:
bootstrap: src/Vendor/autoload.php bootstrap: src/Vendor/autoload.php
php_version: ${{ matrix.php_version }} php_version: ${{ matrix.php_version }}
args: "tests" args: "tests"
php_extensions: apcu

View file

@ -12,13 +12,14 @@ AntCMS is a lightweight CMS system designed for simplicity, speed, and small siz
### How fast is AntCMS? ### 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. 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`. 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? ### 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. 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 ### 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. 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.

View file

@ -7,6 +7,44 @@ use Symfony\Component\Yaml\Exception\ParseException;
class AntCache 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. * Caches a value for a given cache key.
* *
@ -17,17 +55,17 @@ class AntCache
*/ */
public function setCache(string $key, string $content) public function setCache(string $key, string $content)
{ {
$cachePath = AntCachePath . DIRECTORY_SEPARATOR . "{$key}.cache"; switch ($this->cacheType) {
$config = AntConfig::currentConfig(); case self::noCache:
if ($config['enableCache']) { return false;
try { case self::fileCache:
file_put_contents($cachePath, $content); $cachePath = AntCachePath . DIRECTORY_SEPARATOR . "{$key}.cache";
return true; return file_put_contents($cachePath, $content);
} catch (\Exception) { case self::apcuCache:
$apcuKey = $this->cacheKeyApcu . $key;
return apcu_store($apcuKey, $content, 7 * 24 * 60 * 60); // Save it for one week.
default:
return false; return false;
}
} else {
return true;
} }
} }
@ -40,16 +78,21 @@ class AntCache
*/ */
public function getCache(string $key) public function getCache(string $key)
{ {
$cachePath = AntCachePath . DIRECTORY_SEPARATOR . "{$key}.cache"; switch ($this->cacheType) {
$config = AntConfig::currentConfig(); case self::noCache:
if ($config['enableCache']) { return false;
try { case self::fileCache:
return file_get_contents($cachePath); $cachePath = AntCachePath . DIRECTORY_SEPARATOR . "{$key}.cache";
} catch (\Exception) { 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; return false;
}
} else {
return false;
} }
} }
@ -62,12 +105,17 @@ class AntCache
*/ */
public function isCached(string $key) public function isCached(string $key)
{ {
$config = AntConfig::currentConfig(); switch ($this->cacheType) {
if ($config['enableCache']) { case self::noCache:
$cachePath = AntCachePath . DIRECTORY_SEPARATOR . "{$key}.cache"; return false;
return file_exists($cachePath); case self::fileCache:
} else { $cachePath = AntCachePath . DIRECTORY_SEPARATOR . "{$key}.cache";
return false; 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); 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);
}
}
}
}
} }

View file

@ -11,7 +11,7 @@ class AntConfig
'siteInfo', 'siteInfo',
'forceHTTPS', 'forceHTTPS',
'activeTheme', 'activeTheme',
'enableCache', 'cacheMode',
'debug', 'debug',
'baseURL', 'baseURL',
'embed', 'embed',
@ -29,7 +29,7 @@ class AntConfig
], ],
'forceHTTPS' => true, 'forceHTTPS' => true,
'activeTheme' => 'Default', 'activeTheme' => 'Default',
'enableCache' => true, 'cacheMode' => 'auto',
'debug' => true, 'debug' => true,
'baseURL' => $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']), 'baseURL' => $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']),
'embed' => [ 'embed' => [

View file

@ -11,7 +11,7 @@ class AntTwig
public function __construct(string $theme = null) 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'); $this->theme = $theme ?? AntConfig::currentConfig('activeTheme');
if (!is_dir(antThemePath . '/' . $this->theme)) { if (!is_dir(antThemePath . '/' . $this->theme)) {

View file

@ -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. - `siteTitle: AntCMS` - This configuration sets the title of your AntCMS website.
- `forceHTTPS: true` - Set to 'true' by default, enables HTTPs redirection. - `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. - `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. - `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. - `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.

View file

@ -1,7 +1,10 @@
<?php <?php
$cacheDir = __DIR__ . DIRECTORY_SEPARATOR . 'Cache' . DIRECTORY_SEPARATOR;
$di = new RecursiveDirectoryIterator($cacheDir, FilesystemIterator::SKIP_DOTS); require_once __DIR__ . DIRECTORY_SEPARATOR . 'Vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
$ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST); $classMapPath = __DIR__ . DIRECTORY_SEPARATOR . 'Cache' . DIRECTORY_SEPARATOR . 'classMap.php';
foreach ($ri as $file) { $loader = new AntCMS\AntLoader($classMapPath);
$file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath()); $loader->addPrefix('AntCMS\\', __DIR__ . DIRECTORY_SEPARATOR . 'AntCMS');
} $loader->checkClassMap();
$loader->register();
AntCMS\AntCache::clearCache();

View file

@ -15,7 +15,7 @@ class ConfigTest extends TestCase
'siteInfo', 'siteInfo',
'forceHTTPS', 'forceHTTPS',
'activeTheme', 'activeTheme',
'enableCache', 'cacheMode',
'debug', 'debug',
'baseURL' 'baseURL'
); );
@ -28,7 +28,7 @@ class ConfigTest extends TestCase
public function testSaveConfigFailed() public function testSaveConfigFailed()
{ {
$Badconfig = [ $Badconfig = [
'enableCache' => true, 'cacheMode' => 'none',
]; ];
try { try {

View file

@ -2,7 +2,7 @@ siteInfo:
siteTitle: 'AntCMS' siteTitle: 'AntCMS'
forceHTTPS: true forceHTTPS: true
activeTheme: Default activeTheme: Default
enableCache: true cacheMode: auto
debug: true debug: true
baseURL: antcms.org/ baseURL: antcms.org/
embed: embed:

View file

@ -22,7 +22,7 @@ class MarkdownTest extends TestCase
$currentConfig = AntConfig::currentConfig(); $currentConfig = AntConfig::currentConfig();
//Ensure cache is enabled //Ensure cache is enabled
$currentConfig['enableCache'] = true; $currentConfig['cacheMode'] = 'auto';
AntConfig::saveConfig($currentConfig); AntConfig::saveConfig($currentConfig);
for ($i = 0; $i < 10; ++$i) { for ($i = 0; $i < 10; ++$i) {
@ -48,7 +48,7 @@ class MarkdownTest extends TestCase
$currentConfig = AntConfig::currentConfig(); $currentConfig = AntConfig::currentConfig();
//Disable cache //Disable cache
$currentConfig['enableCache'] = false; $currentConfig['cacheMode'] = 'none';
AntConfig::saveConfig($currentConfig); AntConfig::saveConfig($currentConfig);
$totalTime = 0; $totalTime = 0;
@ -62,7 +62,7 @@ class MarkdownTest extends TestCase
$withoutCache = $totalTime / 10; $withoutCache = $totalTime / 10;
//Enable cache //Enable cache
$currentConfig['enableCache'] = true; $currentConfig['cacheMode'] = 'auto';
AntConfig::saveConfig($currentConfig); AntConfig::saveConfig($currentConfig);
$totalTime = 0; $totalTime = 0;