From 31eefd699a0f5183d93b505b137b4fc1e819eedf Mon Sep 17 00:00:00 2001 From: bnomei Date: Fri, 22 Mar 2024 02:55:26 +0000 Subject: [PATCH] :sparkles: improved caching --- README.md | 48 +++++++++++++--- classes/Blueprints/Attributes/Blueprint.php | 2 +- classes/Blueprints/Blueprint.php | 10 ++-- classes/Blueprints/BlueprintCache.php | 57 ++++++++++++++++--- classes/Blueprints/HasBlueprintCache.php | 50 ++++++++++++++++ .../Blueprints/HasBlueprintCacheResolve.php | 8 +++ composer.json | 2 +- index.php | 3 + tests/content/example-2/example.txt | 1 + tests/content/example/example.txt | 9 +++ tests/index.php | 7 ++- tests/site/blueprints/fields/extme.yml | 3 + tests/site/models/example.php | 7 +++ .../aaaa/blueprints/blocks/someblo.yml | 6 ++ .../plugins/aaaa/blueprints/pages/example.yml | 15 +++++ tests/site/plugins/test/models/DynamoPage.php | 38 +++++++++++-- .../site/plugins/test/models/ElephantPage.php | 2 +- .../site/plugins/test/models/ProductPage.php | 2 +- tests/site/templates/example.php | 3 + vendor/composer/autoload_classmap.php | 2 + vendor/composer/autoload_static.php | 2 + vendor/composer/installed.php | 8 +-- 22 files changed, 249 insertions(+), 36 deletions(-) create mode 100644 classes/Blueprints/HasBlueprintCache.php create mode 100644 classes/Blueprints/HasBlueprintCacheResolve.php create mode 100644 tests/content/example-2/example.txt create mode 100644 tests/content/example/example.txt create mode 100644 tests/site/blueprints/fields/extme.yml create mode 100644 tests/site/models/example.php create mode 100644 tests/site/plugins/aaaa/blueprints/blocks/someblo.yml create mode 100644 tests/site/plugins/aaaa/blueprints/pages/example.yml create mode 100644 tests/site/templates/example.php diff --git a/README.md b/README.md index 774791e..be52b7e 100644 --- a/README.md +++ b/README.md @@ -535,26 +535,60 @@ class ElephantPage extends Page } ``` -## Caching and Lazy-loading of blueprints +## Caching and Lazy-loading of PHP based blueprints -The `Blueprint` attribute allows you to set the `cacheable` and `loadPluginsAfter` properties. Both are disabled by default. - -If cached it will only compile the blueprint once and then use the cached version. The cache lasts for the duration of you opcache `opcache.revalidate_freq` settings. +If caching is enabled it will only compile the blueprint once and then use the cached version. The cache lasts for the duration set. ```php #[ - Blueprint(cacheable: true) + // cache for 5 seconds + Blueprint(cache: 5) + + // cache with default 60 seconds + Blueprint() + Blueprint(cache: null) + + // disable + Blueprint(cache: 0) ] ``` -If `loadPluginsAfter` is set to `true` it will compile the blueprint after the kirby instance is ready. This means you can use the `kirby()/site()/page()`-helpers in your blueprints or query the content of the site. +> The default can be set in the config.php of your plugin with the `bnomei.blueprints.expire` option. + + +If `defer` is set to `true` it will compile the blueprint after the kirby instance is ready. This means you can use the `kirby()/site()/page()`-helpers in your blueprints or query the content of the site. Most of the time it makes sense to disable caching when using `defer` since the cache might interfere with the dynamic behaviour you are trying to create. ```php #[ - Blueprint(loadPluginsAfter: true) + // load with system.loadPlugins:after hook and no cache + Blueprint(defer: true, cache: 0) ] ``` +## Caching for PHP and YAML based blueprints + +If you are not using the attributes to define your blueprint definitions you can still enable caching using traits model. One to enable the cache and one to define if it should resolve all fields as much as possible. + +Resolving the fields means to write all meta-data in extracting them using a model instance. So if you are using the Kirby query-strings language or other dynamic values in your blueprints (like the Janitor or some SEO plugins do), you might want to skip resolving. But... if you have a very complex blueprint with many fields and you want to speed up the blueprint loading you might want to resolve them. Especially when using lots of Layouts, Columns & Blocks resolving might be a good idea. + +```php + +**site/models/example.php** +```php + The cache will use the duration as defined in the `bnomei.blueprints.expire` option in the config.php of your plugin. ## Disclaimer diff --git a/classes/Blueprints/Attributes/Blueprint.php b/classes/Blueprints/Attributes/Blueprint.php index 14b0660..72c3c05 100644 --- a/classes/Blueprints/Attributes/Blueprint.php +++ b/classes/Blueprints/Attributes/Blueprint.php @@ -10,7 +10,7 @@ class Blueprint /** * Flag method to be parsed as returning a blueprint definition for the model the method belongs to */ - public function __construct(public bool $cacheable = false, public bool $loadPluginsAfter = false) + public function __construct(public ?int $cache = null, public ?bool $defer = null) { } } diff --git a/classes/Blueprints/Blueprint.php b/classes/Blueprints/Blueprint.php index 1691594..98fc305 100644 --- a/classes/Blueprints/Blueprint.php +++ b/classes/Blueprints/Blueprint.php @@ -14,15 +14,15 @@ class Blueprint { protected static array $loadPluginsAfter = []; - public function __construct(private readonly string $modelClass, private bool $loadAfter = false, private ?bool $cache = null) + public function __construct(private readonly string $modelClass, private ?bool $defer = null, private ?int $cache = null) { $isCacheable = false; $rc = new ReflectionClass($modelClass); foreach ($rc->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { foreach ($method->getAttributes() as $attribute) { if ($attribute->getName() === 'Bnomei\Blueprints\Attributes\Blueprint') { - $isCacheable = $attribute->newInstance()->cacheable; - $this->loadAfter = $attribute->newInstance()->loadPluginsAfter; + $isCacheable = $attribute->newInstance()->cache; + $this->defer = $attribute->newInstance()->defer; break; } } @@ -40,7 +40,7 @@ public function __toString(): string public function toArray(): array { - $blueprint = $this->cache ? BlueprintCache::get($this->modelClass) : null; + $blueprint = $this->cache ? BlueprintCache::get($this->modelClass, null, $this->cache) : null; if ($blueprint) { return $blueprint; } @@ -236,6 +236,6 @@ public static function loadPluginsAfter() public function isLoadAfter(): bool { - return $this->loadAfter; + return $this->defer === true; } } diff --git a/classes/Blueprints/BlueprintCache.php b/classes/Blueprints/BlueprintCache.php index 18007a4..b39d1a8 100644 --- a/classes/Blueprints/BlueprintCache.php +++ b/classes/Blueprints/BlueprintCache.php @@ -29,32 +29,46 @@ public static function cacheDir(): ?string return kirby()->session()->get($key); } - private static function cacheFile(string $key): ?string + protected static function cacheFile(string $key): ?string { + $hash = hash('xxh3', $key); + $hash = substr($hash, 0, 2).'/'.substr($hash, 2); + return static::cacheDir() ? - static::cacheDir().'/'.hash('xxh3', $key).'.cache' + static::cacheDir().'/'.$hash.'.cache' : null; } - public static function get(string $key, $default = null): ?array + public static function exists(string $key, $expire = null): bool { $file = static::cacheFile($key); if (! $file) { - return $default; + return false; + } + if ($expire === null) { + $expire = option('bnomei.blueprints.expire'); // in seconds } - $expire = 5; // seconds - if ($opcacheConfig = opcache_get_configuration()) { - $expire = $opcacheConfig['directives']['opcache.enable'] ? - $opcacheConfig['directives']['opcache.revalidate_freq'] : $expire; + if (! $expire || $expire <= 0) { + return false; } $m = F::modified($file) + $expire >= time(); if (F::exists($file) && $m) { - return Json::read($file); + return true; } if (F::exists($file) && ! $m) { F::remove($file); } + return false; + } + + public static function get(string $key, $default = null, $expire = null): ?array + { + $file = static::cacheFile($key); + if ($file && static::exists($key, $expire)) { + return Json::read($file); + } + return $default; } @@ -69,4 +83,29 @@ public static function getKey(): string { return 'bnomei.blueprints.cache.dir'; } + + public static function preloadCachedBlueprints(): void + { + $kirby = kirby(); + $preload = $kirby->option('bnomei.blueprints.preload'); + if ($preload === false) { + return; + } + + $preloaded = []; + foreach ($preload as $type) { + $blueprints = $kirby->blueprints($type); + foreach ($blueprints as $name) { + $key = $type.'/'.$name; + $blueprint = static::get($key); + if ($blueprint === null) { + continue; + } + $preloaded[$key] = $blueprint; + } + } + $kirby->extend([ + 'blueprints' => $preloaded, + ]); + } } diff --git a/classes/Blueprints/HasBlueprintCache.php b/classes/Blueprints/HasBlueprintCache.php new file mode 100644 index 0000000..2c25446 --- /dev/null +++ b/classes/Blueprints/HasBlueprintCache.php @@ -0,0 +1,50 @@ +blueprint(); + $type = match ($blueprint::class) { + PageBlueprint::class => 'pages', + FileBlueprint::class => 'files', + UserBlueprint::class => 'users', + }; + + return $type.'/'.$blueprint->name(); + } + + public function __destruct() + { + /** @var \Kirby\Cms\ModelWithContent $this */ + $key = $this->blueprintCacheKey(); + if (static::$blueprintCache) { + return; + } + static::$blueprintCache = true; + + if (BlueprintCache::exists($key) === false) { + $blueprint = $this->blueprint(); + $data = $blueprint->toArray(); + if (isset(static::$blueprintCacheResolve)) { + foreach ($blueprint->toArray()['tabs'] as $tabKey => $tab) { + foreach ($tab['columns'] as $columnKey => $column) { + foreach ($column['sections'] as $sectionKey => $section) { + $section = $blueprint->section($sectionKey); + $data['tabs'][$tabKey]['columns'][$columnKey]['sections'][$sectionKey]['fields'] = $section->toArray()['fields']; + } + } + } + } + BlueprintCache::set($key, $data); + } + } +} diff --git a/classes/Blueprints/HasBlueprintCacheResolve.php b/classes/Blueprints/HasBlueprintCacheResolve.php new file mode 100644 index 0000000..92cb32a --- /dev/null +++ b/classes/Blueprints/HasBlueprintCacheResolve.php @@ -0,0 +1,8 @@ + [ 'cache' => true, + 'expire' => 60, // in seconds, null = use opcache duration, 0 to disable + 'preload' => ['pages', 'files', 'users'], ], 'hooks' => [ 'system.loadPlugins:after' => function () { @@ -13,6 +15,7 @@ // and use it on next request \Bnomei\Blueprints\BlueprintCache::rememberCacheDir(); \Bnomei\Blueprints\Blueprint::loadPluginsAfter(); + \Bnomei\Blueprints\BlueprintCache::preloadCachedBlueprints(); }, ], ]); diff --git a/tests/content/example-2/example.txt b/tests/content/example-2/example.txt new file mode 100644 index 0000000..5dc575f --- /dev/null +++ b/tests/content/example-2/example.txt @@ -0,0 +1 @@ +Uuid: 2TNU3cFebBEBQm6H \ No newline at end of file diff --git a/tests/content/example/example.txt b/tests/content/example/example.txt new file mode 100644 index 0000000..57cf5c9 --- /dev/null +++ b/tests/content/example/example.txt @@ -0,0 +1,9 @@ +Text: + +---- + +Blocks: [{"content":{"location":"kirby","image":[],"src":"","alt":"","caption":"","link":"","ratio":"","crop":"false"},"id":"bf3ab53f-f5a2-499c-a684-6b74c779f2eb","isHidden":false,"type":"image"},{"content":{"text":""},"id":"f3fda5bc-d4f6-4351-9378-5ec0f36e933a","isHidden":false,"type":"text"},{"content":{"code":"","language":"text"},"id":"cfb1028f-c85e-4434-92c5-d779dfcc7e77","isHidden":false,"type":"code"}] + +---- + +Uuid: 2TNU3cFebBEBQm6H \ No newline at end of file diff --git a/tests/index.php b/tests/index.php index b8b3209..91ea7dc 100644 --- a/tests/index.php +++ b/tests/index.php @@ -4,4 +4,9 @@ const KIRBY_HELPER_E = false; require __DIR__.'/../vendor/autoload.php'; -echo (new Kirby())->render(); + +$micotime = microtime(true); +$render = (new Kirby())->render(); +$micotime = microtime(true) - $micotime; +header('X-Render-Time: '.number_format($micotime, 3, '.', '').'s'); +echo $render; diff --git a/tests/site/blueprints/fields/extme.yml b/tests/site/blueprints/fields/extme.yml new file mode 100644 index 0000000..3d586af --- /dev/null +++ b/tests/site/blueprints/fields/extme.yml @@ -0,0 +1,3 @@ +type: text +label: extme +placeholder: extme diff --git a/tests/site/models/example.php b/tests/site/models/example.php new file mode 100644 index 0000000..a5ddb45 --- /dev/null +++ b/tests/site/models/example.php @@ -0,0 +1,7 @@ +site(); $user = $kirby->user(); - return Ink::page( - title: 'Dynamo', - columns: [ + $tab1 = Ink::tab('content') + ->columns([ Ink::column('1/2')->fields([ Ink::field('text', label: 'Kirby Version') ->property('placeholder', $kirby->version()), @@ -54,7 +56,31 @@ public static function nameOfThisMethodDoesNotMatterOnlyTheAttribute(): array ->label('Files') ->id('filesyoyo'), ]), - ], + ]); + + $tabs = [ + $tab1, + ]; + + for ($t = 1; $t < 20; $t++) { + $fields = []; + $sections = []; + for ($f = 1; $f < 40; $f++) { + $fields[] = Ink::field('text', label: 'Field '.$t.$f); + } + for ($s = 1; $s < 10; $s++) { + $sections[] = Ink::section(SectionTypes::FILES)->label('Files '.$t.$s); + } + $tabs[] = Ink::tab('tab'.$t) + ->columns([ + Ink::column('1/2')->fields($fields), + Ink::column('1/2')->sections($sections), + ]); + } + + return Ink::page( + title: 'Dynamo', + tabs: $tabs, )->toArray(); } diff --git a/tests/site/plugins/test/models/ElephantPage.php b/tests/site/plugins/test/models/ElephantPage.php index b6ff646..c6d3692 100644 --- a/tests/site/plugins/test/models/ElephantPage.php +++ b/tests/site/plugins/test/models/ElephantPage.php @@ -25,7 +25,7 @@ class ElephantPage extends Page public Field $rightEar; #[ - Blueprint + Blueprint(cache: 10) ] public static function elephantsBlueprint(): array { diff --git a/tests/site/plugins/test/models/ProductPage.php b/tests/site/plugins/test/models/ProductPage.php index 4c38265..653d664 100644 --- a/tests/site/plugins/test/models/ProductPage.php +++ b/tests/site/plugins/test/models/ProductPage.php @@ -38,7 +38,7 @@ class ProductPage extends \Kirby\Cms\Page public Kirby\Content\Field $email; #[ - Blueprint(cacheable: true) + Blueprint(cache: 10) ] public static function nameOfThisMethodDoesNotMatterOnlyTheAttribute(): array { diff --git a/tests/site/templates/example.php b/tests/site/templates/example.php new file mode 100644 index 0000000..d015cfe --- /dev/null +++ b/tests/site/templates/example.php @@ -0,0 +1,3 @@ + $baseDir . '/classes/Blueprints/Attributes/Wizard.php', 'Bnomei\\Blueprints\\Blueprint' => $baseDir . '/classes/Blueprints/Blueprint.php', 'Bnomei\\Blueprints\\BlueprintCache' => $baseDir . '/classes/Blueprints/BlueprintCache.php', + 'Bnomei\\Blueprints\\HasBlueprintCache' => $baseDir . '/classes/Blueprints/HasBlueprintCache.php', + 'Bnomei\\Blueprints\\HasBlueprintCacheResolve' => $baseDir . '/classes/Blueprints/HasBlueprintCacheResolve.php', 'Bnomei\\Blueprints\\HasBlueprintFromAttributes' => $baseDir . '/classes/Blueprints/HasBlueprintFromAttributes.php', 'Bnomei\\Blueprints\\HasFluentSetter' => $baseDir . '/classes/Blueprints/HasFluentSetter.php', 'Bnomei\\Blueprints\\HasProperties' => $baseDir . '/classes/Blueprints/HasProperties.php', diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index ef7c170..287cfd2 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -112,6 +112,8 @@ class ComposerStaticInitf86866b1e656ff06b3e06fa3ac36b4f7 'Bnomei\\Blueprints\\Attributes\\Wizard' => __DIR__ . '/../..' . '/classes/Blueprints/Attributes/Wizard.php', 'Bnomei\\Blueprints\\Blueprint' => __DIR__ . '/../..' . '/classes/Blueprints/Blueprint.php', 'Bnomei\\Blueprints\\BlueprintCache' => __DIR__ . '/../..' . '/classes/Blueprints/BlueprintCache.php', + 'Bnomei\\Blueprints\\HasBlueprintCache' => __DIR__ . '/../..' . '/classes/Blueprints/HasBlueprintCache.php', + 'Bnomei\\Blueprints\\HasBlueprintCacheResolve' => __DIR__ . '/../..' . '/classes/Blueprints/HasBlueprintCacheResolve.php', 'Bnomei\\Blueprints\\HasBlueprintFromAttributes' => __DIR__ . '/../..' . '/classes/Blueprints/HasBlueprintFromAttributes.php', 'Bnomei\\Blueprints\\HasFluentSetter' => __DIR__ . '/../..' . '/classes/Blueprints/HasFluentSetter.php', 'Bnomei\\Blueprints\\HasProperties' => __DIR__ . '/../..' . '/classes/Blueprints/HasProperties.php', diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 1cf8bfe..c195182 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -1,8 +1,8 @@ array( 'name' => 'bnomei/kirby-blueprints', - 'pretty_version' => '4.3.0', - 'version' => '4.3.0.0', + 'pretty_version' => '4.4.0', + 'version' => '4.4.0.0', 'reference' => null, 'type' => 'kirby-plugin', 'install_path' => __DIR__ . '/../../', @@ -11,8 +11,8 @@ ), 'versions' => array( 'bnomei/kirby-blueprints' => array( - 'pretty_version' => '4.3.0', - 'version' => '4.3.0.0', + 'pretty_version' => '4.4.0', + 'version' => '4.4.0.0', 'reference' => null, 'type' => 'kirby-plugin', 'install_path' => __DIR__ . '/../../',