Page Tree

The most important single data structure in all of Mayd is probably the page tree with its implementation as NodeTree.

The node tree is an abstract tree. Both the menu trees, as well as the page tree are built as node trees.

The page tree is important, because it’s not just the hierarchical structure of the website, it’s also cached. It’s important to cache it, because accessing the actual page entries is very expensive and generates an enormous amount of queries. Try to always use the page tree instead of querying for page entities.

Attaching Custom Data

While the base page tree is built by Mayd itself, you can add extra data to any node in the node tree. You can use this data to either display it in your page, or to extend the menu tree.

The actual menu items for the menu is generated from the page tree / menu tree. You can use a visitor to modify the menu items.

Keep in mind that the page tree is cached! That means, don’t add extras to your page tree that can become invalid at different times as the tree itself (or clear the cache manually).

Also everything inside page tree must be serializable, so you should not add entities as extras.

Page Tree Extras

Attaching additional data to the page tree works, by adding extras to the page tree. You need to implement the PageTreeNodeVisitorInterface interface:

class TopicAreaPageTreeVisitor implements PageTreeNodeVisitorInterface
{
    public function generateExtras (PageTreeNode $page) : array
    {
        // you receive the page tree node (the entity) here and need to return the
        // extra array. All extra arrays will be merged, later entries with the same
        // key will override your keys, so watch out!
    }
}

In this example we have a “topic area” system page, that should render our “topic area datasets” as child pages. We want to add these sub pages to the menu (to have them a) in there, and b) to directly get them highlighted if we are on this exact page).

So we first add the integration into the page tree:

class TopicAreaPageTreeVisitor implements PageTreeNodeVisitorInterface
{
    private const EXTRA_KEY = "topic-areas";

    public function generateExtras (PageTreeNode $page) : array
    {
        if ($page instanceof Page && $page->getContent() instanceof TopicAreasContent)
        {
            return [
                // keep in mind: only serializable data, eg. scalars
                self::EXTRA_KEY => [
                    "page" => $page->getId(),
                    "website" => $page->getWebsite()->getId(),
                    "language" => $page->getLocale()->getLanguage()->getId(),
                ],
            ];
        }

        return [];
    }
}

The data must be serializable: we only add scalars, so that works well.

Also the data should only change if the page tree changes, let’s check if we fulfill that requirement:

  • The page tree is for a website, a website has a fixed language, so the language id is safe.
  • The website id itself is safe as well, obviously.
  • The page id is also safe: if the page is moved (to a different website), the website changed, so the cache will automatically be cleared.

Okay, so now we have the topic area data in the page tree. Now we need to attach it to the menu item that represents the “topic area page” in the page tree.

We need to add a ItemVisitor to modify the menu tree. As the ItemVisitor interface and the PageTreeNodeVisitorInterface have no overlap, we can implement both of them in the same class.

If you add extras to the page tree to later pass them on to the menu item, always implement both the ItemVisitor interface and the PageTreeNodeVisitorInterface in the same class.

This keeps the code tidy, perfectly packaged and has a nice abstraction to the rest of the app.

Both the menu items and the page tree has extras. All page tree extras are automatically forwarded to the menu item extras.

Now, let’s build the menu item integration:

class TopicAreaPageTreeVisitor implements PageTreeNodeVisitorInterface
{
    // ...

    /**
     * @inheritDoc
     */
    public function visit (MenuItem $item, array $options) : void
    {
        // fetch the extra we stored above from the menu item.
        $config = $item->getExtra(self::EXTRA_KEY);

        if (!\is_array($config) || ... )
        {
            // some sanity checks here
            return;
        }

        // fetch the page tree + the language entity
        $tree = $this->nodeTreeModel->getPageTreeByWebsiteId($config["website"]);
        $page = $tree->getById($config["page"]);
        $language = $this->languageModel->getById($config["language"]);

        // if the page and language exists...
        if (null === $page || null === $language)
        {
            return;
        }

        // ... fetch the topic areas for this language ...
        foreach ($this->topicAreaModel->getAllByLanguage($language) as $topicArea)
        {
            // ... and append them as child to the menu item that represents the topic area
            $item->createChild($topicArea->getName(), [
                "target" => $this->pageRouter->generate($page, "topic-area", [
                    "slug" => $topicArea->getSlug(),
                ]),
            ]);
        }
    }


    /**
     * @inheritDoc
     */
    public function supports (array $options) : bool
    {
        return true;
    }
}

For more details about how the menu bundle works, read its docs.

Now, as the items are inside the menu item tree, they will automatically be marked as “current” if the URL matches, etc. So all the menu tree integrations just work.

Clearing the Page Tree Cache

You can manually clear any node tree cache, by calling the method and passing the related entity:

// clear cache for tree of website that contains
// this page
$nodeTreeModel->clearCache($page);

// clear cache for tree of website
$nodeTreeModel->clearCache($website);

// clear cache for tree of menu with this entry
$nodeTreeModel->clearCache($menuEntry);

// clear cache for tree of menu
$nodeTreeModel->clearCache($menu