Model with nested children

The setup

I got a custom model (Parent) to be the panel-front for a database backed set of pages. This model is purly for the panel side, and won’t be accesible on the front end, so I can mess with it purly for the panel’s purpose.

For this reason I have a draft page-file representing this panel-front. In the file there is nothing stored other than the page-template (and linked to that its model):

Template: ParentTemplate

I got a blueprint set for that model (Parent) with only a Pages section, which should contain all the database pages.

#partial file for brevety
sections:
  pages:
    headline: Pages
    type: pages
    create:
      - ChildTemplate
    templates:
      - ChildTemplate
    empty: No pages yet
    image: false
    text: "{{page.title}}"
    info: "{{page.date}}"
    layout: list
    parent: page
    sortBy: page.date desc page.title asc

The problem
The database determines the url/slug for each child. This can contain multiple layers/depths (e.g. Parent/somecategory/childname).

On first sight there doesn’t seem to be a problem. When opening the panel-front (Parent) every child is present in the list. But when I open a childpage for editing/viewing, it will crash if the url/slug of that child has more than 1 layer/depth.

So a child with url/sug test will open just fine, but a child with url test/ing cannot be found.

Analysing

The way Kirby obtains their pages on the panel side is by calling API->page which refers after a few calls to App->page() which looks like this:

    /**
     * Returns any page from the content folder
     *
     * @param string|null $id
     * @param \Kirby\Cms\Page|\Kirby\Cms\Site|null $parent
     * @param bool $drafts
     * @return \Kirby\Cms\Page|null
     */
    public function page(?string $id = null, $parent = null, bool $drafts = true)
    {
        if ($id === null) {
            return null;
        }

        $parent = $parent ?? $this->site();

        if ($page = $parent->find($id)) {
            return $page;
        }

        if ($drafts === true && $draft = $parent->draft($id)) {
            return $draft;
        }

        return null;
    }

The $parent->find($id) call will not find anything because the panel-front is a draft.

The $parent->draft($id) is where it should return the correct result, but where something weird happens. The function looks like follows:

/**
     * Searches for a child draft by id
     *
     * @param string $path
     * @return \Kirby\Cms\Page|null
     */
    public function draft(string $path)
    {
        $path = str_replace('_drafts/', '', $path);

        if (Str::contains($path, '/') === false) {
            return $this->drafts()->find($path);
        }

        $parts  = explode('/', $path);
        $parent = $this;

        foreach ($parts as $slug) {
            if ($page = $parent->find($slug)) {
                $parent = $page;
                continue;
            }

            if ($draft = $parent->drafts()->find($slug)) {
                $parent = $draft;
                continue;
            }

            return null;
        }

        return $parent;
    }

Basically it splits the slug/url in portions and tries to find the childpage iteratively. On its own there is not really a problem, until a parent has children with a multi-layer/depths url/slug.

In my custom model I can override the find function, which could handle it perfeclty (and it would, if given the full url), but since it only tries to find a single portion of the url/slug. There is no clean way to handle this from a Model-class unfortunately.

Additional thoughts

The decision to split the url/slug is made in the App class basically, which makes it impossible to catch it without override kirby-base classes (something I don’t want to do unless absolutely necessary).

I thought about making dumy pages for the layers in between the panel-front (Parent) and the absolute child. This would solve the panel-url-find-problem, but that would also mean the Pages section would mess up. (Since both rely on the children function in a Model.

Question

I can think of several ways to solve this on Kirby’s end, but that is something I’ll leave up to the devs.
In the meantime, is there a clean way to solve this on my end?

Ps. If it is better to ask this on another platform (since it seems like different members are active on different platforms) please let me know, so I can put up the question there!

If I understand correctly, you have direct children of a model without parents for the nested layers. I think that is a use case that is not supported, and you would have to create parent models for the nested children.

That’s too bad, as it will mess up the Pages section when every ‘in-between-parent’ is a fysical Page object.

I may have found a workaround for that, but it feels messy (and creates some overhead).


As a sidenote, this system is not multi-language friendly (as I didn’t need it).

/** A DummyPage to represent a in-between layer */
class DummyPage extends Page
{
    public function children()
    {
        if (is_a($this->children, 'Kirby\Cms\Pages') === true)
            return $this->children;
        else return $this->children = Pages::factory([], $this);
    }

    public function content(?string $languageCode = null)
    {
        throw new Exception('Unsupported operation');
    }
}
class ParentPage extends Page
{
    /** Customized searchpool */
    protected $children_search_pool;

    public function children()
    {
        $this->loadChildren();
        return $this->children;
    }

    public function children_search_pool()
    {
        $this->loadChildren();
        return $this->children_search_pool;
    }

    protected function loadChildren()
    {
        if (is_a($this->children, 'Kirby\Cms\Pages') === true || is_a($this->children_search_pool, 'Kirby/Cms/Pages') === true)
            return;

        $parent = $this;
        $site = $this->site();
        $kirby = $this->kirby();

        $rs = /* Omitted for brevity, this should querry a database for the child-pages */

        $children = new Pages([], $parent);
        $search_pool = new Pages([], $parent);

        if ($rs != false && size($rs) > 0) {
            foreach ($rs as $r) {
                $url = $r->get('slug');
                if (Utility::isEmpty($url))
                    throw new InvalidArgumentException('');



                $page = Page::factory([
                    'kirby' => $kirby,
                    'site' => $site,
                    'parent' => $parent,

                    'slug'     => $url,
                    'template' => 'ChildTemplate',
                    'model'    => 'ChildTemplate',

                    'content'  => [
                        'title' => $r->get('title'),
                        'child_id' => $r->get('id'),
                        'child_date' => $r->get('date'),
                        'child_content_exceprt' => $r->get('content_excerpt'),
                        'db_loaded' => false /* flag for partial loading, to prevent a complete database from being querried */
                    ]
                ]);



                $slugs = explode('/', $url);
                if (size($slugs) > 1) {
                    $sp_pp = static::find_or_create_parents($this, $search_pool, substr($url, 0, strrpos($url, '/')));
                    if ($sp_pp)
                        $sp_pp->children()->data[$page->id()] = $page;
                    else $search_pool->data[$page->id()] = $page;
                    $children->data[$page->id()] = $page;
                } else {
                    $children->data[$page->id()] = $page;
                    $search_pool->data[$page->id()] = $page;
                }
            }
        }

        $this->children_search_pool = $search_pool;
        $this->children = $children;
    }

    protected static function find_or_create_parents(Page $parent, Pages $pool, ?string $slug): ?Page
    {
        if (Utility::isEmpty($slug))
            return null;

        $parts = explode('/', $slug);
        $cp = $parent;
        $cps = $pool;
        foreach ($parts as $p) {
            $tr = $cps->find($p);

            if ($tr == null) {
                $tr = new DummyPage([
                    'parent' => $cp,
                    'slug' => $p
                ]);
                $cps->data[$tr->id()] = $tr;
            }
            $cp = $tr;
            $cps = $cp->children();
        }

        return $cp;
    }

    public function find(...$arguments)
    {
        $result = $this->children()->find(...$arguments);

        if ($result != null)
            return $result;

        return $this->children_search_pool()->find(...$arguments);
    }
}