Page Properties

If you want to extend the core pages with custom properties, you should use the page properties.

Data Structure

In terms of data structure you have basically two choices: base content fields or a separate page properties entities.

Base Content Fields

The main idea is that you just add a base content class of which every content extend from (with a base content form) or even better: use a base properties trait with an interface.

The trait:

trait AppContentTrait
{
    /**
     * @Column(...)
     */
    private ?string $headline = null;

    /**
     * @Column(...)
     */
    private ?string $slogan = null;


    public function getHeadline () : ?string { /* ... */ }
    public function setHeadline () : ?string { /* ... */ }
    public function getSlogan () : ?string { /* ... */ }
    public function setSlogan () : ?string { /* ... */ }
}

The interface:

interface AppContentInterface
{
    public function getHeadline () : ?string;
    public function setHeadline () : ?string;
    public function getSlogan () : ?string;
    public function setSlogan () : ?string;
}

Now you just add the trait to every content, and also implement the interface. This way you have a pretty flexible implementation.

Pros

  • Pretty straightforward to implement.
  • You can add these fields to the main ContentForm so that they are directly editable when creating a page.

Cons

  • Quite inflexible as they are more or less attached to every Content.
  • You can forget to add it to your Content.
  • You can only add these properties to page tree nodes with a Content, so only to Page s.
  • Only one system can add these properties to a page, as you can only extend from one Content.

You should avoid overloading your base content form with too many fields. Try to opt for a page properties backend tab instead (see below).

Page Properties Entities

The more flexible way is to add a separate page properties entity. This entity is 1:1 linked to a page tree node.

The entity:

namespace App\Entity\PageProperties;

class NavigationPageProperties
{
    use IdTrait;

    /**
     * @Column(...)
     */
    private bool $inSubNavigation = false;

    /**
     * @ORM\OneToOne(targetEntity="Mayd\Foundation\Entity\Page\PageTreeNode")
     * @ORM\JoinColumn(name="page_tree_node_id")
     */
    private PageTreeNode $page;

    /**
    */
    public function __construct (PageTreeNode $page)
    {
        $this->page = $page;
    }
}

This entity can now structurally be attached to every page tree node.

To persist this page properties entity, you need to add a model for it.

namespace App\Model\PageProperties;

class NavigationPagePropertiesModel implements EventSubscriberInterface
{
    /**
     *
     */
    public function getProperties (PageTreeNode $page) : NavigationPageProperties
    {
        $properties = $this->repository->findOneBy(["page" => $page]);

        if (null === $properties)
        {
            $properties = new NavigationPageProperties($page);
            $this->entityManager->persist($properties);
        }

        return $properties;
    }


    /* you also need to properly clean up the entity */

    /**
     * @inheritDoc
     */
    public static function getSubscribedEvents ()
    {
        return [
            PageTreeNodeRemoveEvent::class => "onPageRemove",
        ];
    }


    /**
     * @internal
     */
    public function onPageRemove (PageTreeNodeRemoveEvent $event)
    {
        $properties = $this->repository->findOneBy(["page" => $event->getPageTreeNode()]);

        if (null !== $properties)
        {
            $this->entityManager->remove($properties);
        }
    }
}

Pros

  • Very flexible.
  • Your page properties form / controller can decide whether a given page tree node has these properties.
  • Can be attached to any page tree node, so Link s and Section s work as well.

Cons

  • A bit more code.
  • An additional indirection in the code increases the complexity.

Integrating the Properties

After we have finished preparing the data structure, we need to integrate our properties in Mayd.

Integrate Backend Forms

To integrate your page properties in the backend, you can add an additional page properties tab in the backend.

If you chose to use the “base content class / trait” data structure and already included the fields in the base content form you can skip this step.

Example page properties backend
An example page properties backend and how it will look like. With the following code, you will add a new entry on the left side. Your handle() method below has to return a response containing the HTML for the right box.

For this you just need to implement a PagePropertiesHandlerInterface, or – more conveniently – just extend the AbstractPagePropertiesHandler:

namespace App\Page\Properties;

use Mayd\Foundation\Entity\Page\PageTreeNode;
use Mayd\Pages\Page\PageProperties\AbstractPagePropertiesHandler;

class NavigationPagePropertiesHandler extends AbstractPagePropertiesHandler
{
    private NavigationPagePropertiesModel $model;


    /**
    * @inheritDoc
    */
    public function __construct (ContainerInterface $container, NavigationPagePropertiesModel $model)
    {
        parent::__construct($container);
        $this->model = $model;
    }


    /**
     * Returns an unique key to identify this specific page properties handler.
     * Must be URL safe, so basically just a-z 0-9 -_.
     *
     * Just like everywhere else: avoid using any prefixes here like "app.".
     */
    public static function getKey () : string
    {
        return "navigation";
    }


    /**
     * This method is supposed to handle the response to the backend page for these page properties.
     *
     * You must return a response. This response is then embedded in the larger page, so don't render the complete
     * backend but just the content.
     *
     * If you are mainly displaying a form, just use the base form template as seen below.
     */
    public function handle (PageTreeNode $page, Request $request, string $url) : Response
    {

        $properties = $this->model->getProperties($page);

        $form = $this->createForm(NavigationPagePropertiesForm::class, $properties, [
            "action" => $url,
            "method" => "post",
        ]);

        if ($form->handleRequest($request)->isSubmitted() && $form->isValid())
        {
            $this->model->update($properties)->flush();

            $this
                ->buildToast("page.properties.navigation.toast.updated")
                ->positive()
                ->finish();

            return $this->redirect($url);
        }

        return $this->render("@MaydPages/page/properties/properties-form.html.twig", [
            "page" => $page,
            "form" => $form->createView(),
            "pageTitle" => "page-properties.navigation.headline",
        ]);
    }


    /**
     * This method receives the page tree node and has to decide whether the page properties apply to
     * the given page tree node.
     *
     * You can add any check here, like `instanceof Section` or level checks.
     */
    public function isEnabled (PageTreeNode $page) : bool
    {
        return true;
    }
}

You can group page property tabs in the backend by overriding the getGroup() method in your page properties handler. The page properties are then grouped under these group labels.

Integrate into Page Data Tree

The page data is mainly handled in a normalized page data tree, that is heavily cached and use throughout Mayd. If you need your page properties somewhere except when viewing this exact page, you should serialize your page properties data into the page data tree.

To do this, you implement the PageTreeNodeVisitorInterface:

namespace App\Integration\PageTree;

class NavigationPropertiesPageTreeVisitor implements PageTreeNodeVisitorInterface
{
    private NavigationPagePropertiesModel $navigationPagePropertiesModel;


    /**
     */
    public function __construct (NavigationPagePropertiesModel $navigationPagePropertiesModel)
    {
        $this->navigationPagePropertiesModel = $navigationPagePropertiesModel;
    }

    /**
     * @inheritDoc
     */
    public function generateExtras (PageTreeNode $page) : array
    {
        $properties = $this->navigationPagePropertiesModel->getProperties($page);

        return [
            "inSubNavigation" => $properties->isInSubNavigation(),
        ];
    }
}

Now, when traversing your tree and accessing the page tree data, you can access your field via $pageData->getExtra("inSubNavigation").