Content loss on pages with virtual data

I think I have a solid solution for you, but we will need to test this some more together and see if it really fits. Our cookbook article is unfortunately no longer valid and we need to update that too once we are sure that my solution works properly.

With our new Storage architecture, we can now create a simple mixed content storage handler. This will combine the default PlainTextStorage with some additional virtual values. The big advantage here is that we get everything out of the box for free if the storage handler is written correectly (translations, versions, etc.)

This new MixedStorageHandler is only needed once. E.g. you can create it as a plugin (site/plugins/mixed-storage/index.php) and can then be reused for multiple virtual content scenarios.

<?php
namespace Kirby\Content\MixedStorage;

use Kirby\Cms\Language;
use Kirby\Content\PlainTextStorage;
use Kirby\Content\VersionId;

class MixedStorage extends PlainTextStorage
{
    protected array $virtual = [];

    /**
     * Read the original content from disk and merge it with the virtual content
     */
	public function read(VersionId $versionId, Language $language): array
	{
        $content = parent::read($versionId, $language);

        return [
            ...$this->readVirtual($versionId, $language),
            ...$content,
        ];
	}

    /**
     * Check if the page exists on disk and otherwise check if there is any virtual content
     */
    public function exists(VersionId $versionId, Language $language): bool
    {
        return parent::exists($versionId, $language) || $this->readVirtual($versionId, $language) !== [];
    }

    /**
     * Read virtual content for a given version and language from our
     * in-memory storage array
     */
    public function readVirtual(VersionId $versionId, Language $language): array
    {
        return $this->virtual[$versionId->value()][$language->code()] ?? [];
    }

    /**
     * Write virtual content for a given version and language to our
     * in-memory storage array
     */
    public function writeVirtual(VersionId $versionId, Language $language, array $data): void
    {
        $this->virtual[$versionId->value()][$language->code()] = $data;
    }
}

With this new Storage class, it’s now actually pretty easy to create virtual pages that can still have content on disk. I’ve adjusted our example from the cookbook article, but it’s totally applicable to the shopify example above as well.

<?php

use Kirby\Content\MixedStorage;
use Kirby\Cms\Language;
use Kirby\Cms\Pages;
use Kirby\Content\Storage;
use Kirby\Content\VersionId;
use Kirby\Toolkit\A;
use Kirby\Uuid\Uuid;

class AnimalsPage extends Page
{
	public function children(): Pages
	{
		if ($this->children instanceof Pages) {
			return $this->children;
		}

		$csv   = csv($this->root() . '/animals.csv', ';');
		$pages = new Pages();

		foreach ($csv as $animal) {
			$slug = Str::slug($animal['Scientific Name']);

			// No need to check for existing pages here. We can
			// simply assume that some of them might already be on disk
			// and some of them are not there yet.
			$page = Page::factory([
				'slug'     => $slug,
				'template' => 'animal',
				'model'    => 'animal',
				'parent'   => $this,
				'num'      => 0,
			]);

			// Switch to our new storage handler to keep all the default
			// features for pages on disk, but also add the virtual content layer
			$page->changeStorage(MixedStorage::class);

			// Write virtual content to our in-memory storage array
			$page->storage()->writeVirtual(
				versionId: VersionId::latest(),
				language: Language::ensure('default'),
				data: [
					'title'       => $animal['Scientific Name'],
					'commonName'  => $animal['Common Name'],
					'description' => $animal['Description'],
					'uuid'        => Uuid::generate(),
				]
			);

			$pages->add($page);
		}

		return $this->children = $pages;
	}
}

The cool part about this solution is that we no longer need that additional subpages method and we also don’t need to check for existing pages. We can also extend this very easily to add virtual content for other versions (changes) or translations by calling $page->storage()->writeVirtual() with a different VersionId or a different Language object.

3 Likes