Skip to content

Commit

Permalink
✨ content cache for files and users
Browse files Browse the repository at this point in the history
Signed-off-by: bnomei <[email protected]>
  • Loading branch information
bnomei committed Jul 10, 2022
1 parent 46d9ec8 commit 2f32523
Show file tree
Hide file tree
Showing 19 changed files with 764 additions and 283 deletions.
61 changes: 48 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![Maintainability](https://flat.badgen.net/codeclimate/maintainability/bnomei/kirby3-boost)](https://codeclimate.com/github/bnomei/kirby3-boost)
[![Twitter](https://flat.badgen.net/badge/twitter/bnomei?color=66d9ef)](https://twitter.com/bnomei)

Boost the speed of Kirby by having content files of pages cached, with automatic unique ID, fast lookup and Tiny-URL.
Boost the speed of Kirby by having content files of files/pages/users cached, with automatic unique ID for pages, fast lookup and Tiny-URL.

## Commercial Usage

Expand Down Expand Up @@ -90,6 +90,40 @@ echo $count . ' Pages have been boosted.';

Congratulations! Now your project is boosted.

### User Models

Starting with version 1.9 you can also cache the content files of user models using the respective traits/extends in your custom models via a custom plugin.

```php
class AdminUser extends \Kirby\Cms\User
{
use \Bnomei\UserHasBoost;
}

Kirby::plugin('myplugin/user', [
'userModels' => [
'admin' => AdminUser::class, // admin is default role
],
]);
```

### File Models

Starting with version 1.9 you can use a setting to tell the plugin to monkey patch the core `Files` class with content cache support. You can only turn this on for all files at once since Kirby does not allow custom File models. It would need to read content file first which would defeat the purpose for a content cache anyway.

**site/config/config.php**
```php
<?php

return [
// other options
'bnomei.boost.patch.files' => true, // default: false
```

### Easier loading of custom models, blueprints, ...

When you use boost your project you might end up with a couple of custom models in a plugin. You can use my [autoloader helper](https://github.com/bnomei/autoloader-for-kirby) to make registering these classes a bit easier. It can also load blueprints, classes, collections, controllers, blockModels, pageModels, routes, api/routes, userModels, snippets, templates and translation files.

## Usage

### Page from Id
Expand Down Expand Up @@ -342,18 +376,19 @@ $boostedCount = site()->boost();

## Settings

| bnomei.boost. | Default | Description |
|---------------------------|----------------|---------------------------|
| fieldname | `'boostid'` | change name of loaded field |
| expire | `0` | expire in minutes for all caches created |
| read | `true` | read from cache |
| write | `true` | write to cache |
| drafts | `true` | index drafts |
| fileModifiedCheck | `false` | expects file to not be altered outside of kirby |
| index.generator | callback | the uuid genertor |
| tinyurl.url | callback | returning `site()->url()`. Use htaccess on that domain to redirect `RewriteRule (.*) http://www.bnomei.com/x/$1 [R=301]` |
| tinyurl.folder | `x` | Tinyurl format: yourdomain/{folder}/{hash} |
| updateIndexWithHooks | `true` | disable this when batch creating lots of pages |
| bnomei.boost. | Default | Description |
|---------------------------|-------------|--------------------------------------------------------------------------------------------------------------------------|
| fieldname | `'boostid'` | change name of loaded field |
| expire | `0` | expire in minutes for all caches created |
| read | `true` | read from cache |
| write | `true` | write to cache |
| drafts | `true` | index drafts |
| patch.files | `false` | monkey patch Files Class to do content caching |
| fileModifiedCheck | `false` | expects file to not be altered outside of kirby |
| index.generator | callback | the uuid genertor |
| tinyurl.url | callback | returning `site()->url()`. Use htaccess on that domain to redirect `RewriteRule (.*) http://www.bnomei.com/x/$1 [R=301]` |
| tinyurl.folder | `x` | Tinyurl format: yourdomain/{folder}/{hash} |
| updateIndexWithHooks | `true` | disable this when batch creating lots of pages |

## External changes to content files

Expand Down
16 changes: 16 additions & 0 deletions classes/BoostCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use Kirby\Cache\MemoryCache;
use Kirby\Cache\MemCached;
use Kirby\Cache\NullCache;
use Kirby\Filesystem\F;
use Kirby\Toolkit\Str;

final class BoostCache
{
Expand All @@ -24,6 +26,7 @@ public static function singleton(): Cache
self::$singleton->flush();
}
*/
self::patchFilesClass();

return self::$singleton;
}
Expand Down Expand Up @@ -127,4 +130,17 @@ public static function redis(array $options = [])//: Cache
}
return null;
}

public static function patchFilesClass() {
if (option('bnomei.boost.patch.files')) {
$filesClass = kirby()->roots()->kirby() . '/src/Cms/Files.php';
if (F::exists($filesClass) && F::isWritable($filesClass)) {
$code = F::read($filesClass);
if (Str::contains($code, '\Bnomei\BoostFile::factory') === false) {
$code = str_replace('File::factory(', '\Bnomei\BoostFile::factory(', $code);
F::write($filesClass, $code);
}
}
}
}
}
10 changes: 10 additions & 0 deletions classes/BoostFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Bnomei;

class BoostFile extends \Kirby\Cms\File
{
use FileHasBoost;
}
10 changes: 10 additions & 0 deletions classes/BoostUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Bnomei;

class BoostUser extends \Kirby\Cms\User
{
use UserHasBoost;
}
216 changes: 216 additions & 0 deletions classes/FileHasBoost.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
<?php

declare(strict_types=1);

namespace Bnomei;

use Kirby\Cms\File;
use Kirby\Toolkit\A;

trait FileHasBoost
{
/** @var bool */
private $boostWillBeDeleted;

/*
public static function create(array $props): File
{
$fieldname = option('bnomei.boost.fieldname');
if (!A::get($props['content'], $fieldname)) {
$boostid = option('bnomei.boost.index.generator')();
// make 100% sure its unique
while (BoostIndex::singleton()->findByBoostId($boostid, false)) {
$boostid = option('bnomei.boost.index.generator')();
}
$props['content'][$fieldname] = $boostid;
}
return parent::create($props);
}
*/

public function checkModifiedTimestampForContentBoost(): bool
{
return option('bnomei.boost.fileModifiedCheck');
}

public function setBoostWillBeDeleted(bool $value): void
{
$this->boostWillBeDeleted = $value;
}

public function hasBoost(): bool
{
return true;
}

public function isContentBoosted(string $languageCode = null): bool
{
return $this->readContentCache($languageCode) !== null;
}

/*
public function forceNewBoostId(bool $overwrite = false, ?string $id = null)
{
if ($overwrite || $this->boostIDField()->isEmpty()) {
$boostid = $id ?? option('bnomei.boost.index.generator')();
// make 100% sure its unique
while (BoostIndex::singleton()->findByBoostId($boostid, false)) {
$boostid = option('bnomei.boost.index.generator')();
}
$fieldname = option('bnomei.boost.fieldname');
kirby()->impersonate('kirby');
return $this->update([
$fieldname => $boostid,
]);
}
return $this;
}
*/

public function contentBoostedKey(string $languageCode = null): string
{
$key = strval(crc32($this->id()));
if (! $languageCode) {
$languageCode = kirby()->languages()->count() ? kirby()->language()->code() : null;
}
if ($languageCode) {
$key = $key . '-' . $languageCode;
}

return $key;
}

public function isContentCacheExpiredByModified(string $languageCode = null): bool
{
$cache = BoostCache::singleton();
if (! $cache) {
return true;
}

$modifiedCache = $cache->get(
$this->contentBoostedKey($languageCode).'-modified',
null
);
if (!$modifiedCache) {
return true;
}

$modified = $this->modified();
// in rare case the file does not exist or is not readable
if ($modified === false) {
return true;
}
// otherwise compare
if ($modifiedCache && intval($modifiedCache) < intval($modified)) {
return true;
}

return false;
}

public function readContentCache(string $languageCode = null): ?array
{
if ($this->checkModifiedTimestampForContentBoost()) {
if ($this->isContentCacheExpiredByModified($languageCode)) {
return null;
}
}

return BoostCache::singleton()->get(
$this->contentBoostedKey($languageCode) . '-content',
null
);
}

public function readContent(string $languageCode = null): array
{
// read from boostedCache if exists
$data = option('bnomei.boost.read') === false || option('debug') ? null : $this->readContentCache($languageCode);

// read from file and update boostedCache
if (! $data) {
$data = parent::readContent($languageCode);
if ($data && $this->boostWillBeDeleted !== true) {
$this->writeContentCache($data, $languageCode);
}
}

return $data;
}

public function writeContentCache(?array $data = null, string $languageCode = null): bool
{
$cache = BoostCache::singleton();
if (! $cache || option('bnomei.boost.write') === false) {
return true;
}

$modified = $this->modified();

// in rare case file does not exists or is not readable
if ($modified === false) {
return false; // try again another time
}

$cache->set(
$this->contentBoostedKey($languageCode) . '-modified',
$modified,
option('bnomei.boost.expire')
);

return $cache->set(
$this->contentBoostedKey($languageCode) . '-content',
$data,
option('bnomei.boost.expire')
);
}

public function writeContent(array $data, string $languageCode = null): bool
{
// write to file and cache
return parent::writeContent($data, $languageCode) &&
$this->writeContentCache($data, $languageCode);
}

public function deleteContentCache(): bool
{
$cache = BoostCache::singleton();
if (! $cache) {
return true;
}

$this->setBoostWillBeDeleted(true);

foreach (kirby()->languages() as $language) {
$cache->remove(
$this->contentBoostedKey($language->code()) . '-content'
);
$cache->remove(
$this->contentBoostedKey($language->code()).'-modified'
);
}
$cache->remove(
$this->contentBoostedKey() . '-content'
);
$cache->remove(
$this->contentBoostedKey().'-modified'
);

return true;
}

public function delete(bool $force = false): bool
{
$cache = BoostCache::singleton();
if (! $cache) {
return parent::delete($force);
}

$success = parent::delete($force);
$this->deleteContentCache();

return $success;
}
}
Loading

0 comments on commit 2f32523

Please sign in to comment.