Creating custom value types

In Netgen Layouts, (block) item is a generic concept in a way that blocks do not (and should not) care what kind of items are put inside the blocks. To achieve this, a block item is a wrapper around a value which comes from your CMS. For example, in Ibexa CMS integration, Netgen Layouts supports two types of values: Ibexa location and Ibexa content.

To be able to create block items from your own domain objects, you need to create and register your own custom value type. Value type has three purposes:

  • Loading of your domain object from the CMS by its ID by using a value loader
  • Handling the domain objects provided by your custom query types by using a value converter
  • Generating the URL to the domain object by using a value URL generator

Registering a new value type

To be able to use your domain objects inside Netgen Layouts as block items, you need to register a new value type in the configuration. To do so, you need to provide a unique identifier for your value type and a human readable name:

netgen_layouts:
    value_types:
        my_value_type:
            name: 'My value type'

This configuration registers a new value type in the system with my_value_type identifier.

Implementing a value loader

Note

Support for manual items can be disabled through configuration of value types with the following config:

netgen_layouts:
    value_types:
        my_value_type:
            name: 'My value type'
            manual_items: false

In such cases, implementing a value loader as well as Content Browser support is not needed.

A value loader is an object responsible for loading your domain object by its ID or its remote ID. It is an implementation of Netgen\Layouts\Item\ValueLoaderInterface which provides two methods, load and loadByRemoteId.

load method takes the ID of the domain object and should simply return the object once loaded or null if the object could not be loaded.

loadByRemoteId method takes the remote ID of the domain object and again, should simply return the object once loaded or null if the object could not be loaded.

Note

Remote ID of the object is usually an ID which identifies a same domain object in different databases (dev, staging, production). For example, since regular autoincremented primary keys can be different for the same domain object in development and production databases, remote ID would be used to uniquely and consistently identify the same object in both databases.

The following is an example implementation of a value loader:

<?php

declare(strict_types=1);

namespace App\Item\ValueLoader;

use Netgen\Layouts\Item\ValueLoaderInterface;
use Throwable;

final class MyValueTypeLoader implements ValueLoaderInterface
{
    public function load($id)
    {
        try {
            return $this->myBackend->loadMyObject($id);
        } catch (Throwable $t) {
            return null;
        }
    }

    public function loadByRemoteId($remoteId)
    {
        try {
            return $this->myBackend->loadMyObjectByRemoteId($remoteId);
        } catch (Throwable $t) {
            return null;
        }
    }
}

Once implemented, you need to register the loader in Symfony DI container:

app.layouts.value_loader.my_value_type:
     class: App\Item\ValueLoader\MyValueTypeLoader
     tags:
         - { name: netgen_layouts.cms_value_loader, value_type: my_value_type }

Notice that the service is tagged with netgen_layouts.cms_value_loader DI tag which has a value_type attribute. This attribute needs to have a value equal to your value type identifier.

Note

If you are using autoconfiguration in your Symfony project on PHP 8.1, you don’t have to manually create a service configuration in your config. Instead, you can use a PHP 8 attribute to mark the value loader class as such:

<?php

declare(strict_types=1);

namespace App\Item\ValueLoader;

use Netgen\Layouts\Attribute\AsCmsValueLoader;
use Netgen\Layouts\Item\ValueLoaderInterface;

#[AsCmsValueLoader('my_value_type')]
final class MyValueTypeLoader implements ValueLoaderInterface
{
    ...
}

Implementing Content Browser support

To be able to actually select the items from the CMS and add them to your blocks, you also need to implement a Netgen Content Browser backend.

To automatically recognize which backend is responsible for which value types, you need to make sure that the identifier of the item in the Netgen Content Browser backend you implemented is the same as the identifier of the value type you configured above.

Implementing a value converter

As you’re probably aware, query types need not worry themselves about returning PHP objects specific to Netgen Layouts to work. Instead, they simply return domain objects which are then converted by Netgen Layouts into block items.

Converting the domain objects to Netgen Layouts items is done through so called value converters and every value type needs to have a value converter implemented. Value converter should implement Netgen\Layouts\Item\ValueConverterInterface, which provides methods that return the data used by Netgen Layouts to work with block items, like the ID of the object, name and if the object is considered visible in your CMS.

Method supports should return if the value converter supports the given object. Usually, you will check if the provided object is of correct interface. This makes it possible to handle different types of objects in the same value converter. For example, in Ibexa CMS, Content and ContentInfo are two different objects that represent the same piece of content in the CMS, but with different usecases in mind.

Method getValueType should simply return the identifier of the value type you choose when activating the value type in the configuration.

An example implementation of a value converter might look something like this:

<?php

declare(strict_types=1);

namespace App\Item\ValueConverter;

use App\MyValue;
use Netgen\Layouts\Item\ValueConverterInterface;

final class MyValueTypeConverter implements ValueConverterInterface
{
    public function supports($object): bool
    {
        return $object instanceof MyValue;
    }

    public function getValueType($object): string
    {
        return 'my_value_type';
    }

    public function getId($object)
    {
        return $object->id;
    }

    public function getRemoteId($object)
    {
        return $object->remoteId;
    }

    public function getName($object): string
    {
        return $object->name;
    }

    public function getIsVisible($object): bool
    {
        return $object->isVisible();
    }

    public function getObject($object): object
    {
        $object->param = 'value';

        return $object;
    }
}

Once implemented, you need to register the converter in Symfony DI container and tag it with netgen_layouts.cms_value_converter tag:

app.layouts.value_converter.my_value_type_content:
     class: App\Item\ValueConverter\MyValueTypeConverter
     tags:
         - { name: netgen_layouts.cms_value_converter }

Note

If you are using autoconfiguration in your Symfony project on PHP 8.1, you don’t have to manually create a service configuration in your config. Instead, you can use a PHP 8 attribute to mark the value converter class as such:

<?php

declare(strict_types=1);

namespace App\Item\ValueConverter;

use Netgen\Layouts\Attribute\AsCmsValueConverter;
use Netgen\Layouts\Item\ValueConverterInterface;

#[AsCmsValueConverter]
final class MyValueTypeConverter implements ValueConverterInterface
{
    ...
}

Implementing a value URL generator

To generate the links to your domain objects in your blocks, you can use nglayouts_item_path Twig function in your Twig templates. This function internally forwards the URL generation to the correct value URL generator based on the value type of the item. To generate the URL for your value type, simply implement the Netgen\Layouts\Item\ValueUrlGeneratorInterface, which provides a single method called generate responsible for generating the URL.

Note

generate method should return the full path to the item, including the starting slash, not just a slug.

An example implementation might use the Symfony router and generate the URL based on the object ID:

<?php

declare(strict_types=1);

namespace App\Item\ValueUrlGenerator;

use Netgen\Layouts\Item\ValueUrlGeneratorInterface;

final class MyValueTypeUrlGenerator implements ValueUrlGeneratorInterface
{
    public function generate($object): ?string
    {
        return $this->router->generate(
            'my_custom_route',
            ['id' => $object->id],
        );
    }
}

Once implemented, you need to register the URL generator in Symfony DI container:

app.layouts.value_url_generator.my_value_type:
     class: App\Item\ValueUrlGenerator\MyValueTypeUrlGenerator
     tags:
         - { name: netgen_layouts.cms_value_url_generator, value_type: my_value_type }

Notice that the service is tagged with netgen_layouts.cms_value_url_generator DI tag which has a value_type attribute. This attribute needs to have a value equal to your value type identifier.

Note

If you are using autoconfiguration in your Symfony project on PHP 8.1, you don’t have to manually create a service configuration in your config. Instead, you can use a PHP 8 attribute to mark the value generator class as such:

<?php

declare(strict_types=1);

namespace App\Item\ValueUrlGenerator;

use Netgen\Layouts\Attribute\AsCmsValueUrlGenerator;
use Netgen\Layouts\Item\ValueUrlGeneratorInterface;

#[AsCmsValueUrlGenerator('my_value_type')]
final class MyValueTypeUrlGenerator implements ValueUrlGeneratorInterface
{
    ...
}

Implementing item templates

Once a custom value type is implemented, it’s time to implement Twig templates that will be used to render the item that holds the value.

Just like with block templates, for rendering an item, you need to implement two templates, one for backend (layout editing app) and one for frontend.

Implementing a backend template

A backend template, or rather, template for layout editing app is simple. It receives the item in question in item variable and can be used to render the item name and item image. The basic structure of the template looks like this:

<div class="image">
    <img src="/path/to/image.jpg" />
</div>

<div class="name">
    <p><a href="{{ nglayouts_item_path(item, 'admin') }}" target="_blank" rel="noopener noreferrer">{{ item.name }}</a></p>
</div>

Rendering an item name and URL works for all items, as long as you implemented proper value URL generators and converters. Rendering an image is left for you, as often it requires additional steps in contrast to just outputting the image path.

Registering the backend template is done via the view config:

netgen_layouts:
    view:
        item_view:
            app:
                my_value:
                    template: "@App/app/item/view/my_value.html.twig"
                    match:
                        item\value_type: my_value

Implementing a frontend template

Just as with the backend template, frontend template receives the item in question via item variable. Frontend templates depend on your design, so there’s little sense in providing an example implementation, but once you implement your frontend template, you can register it with:

netgen_layouts:
    view:
        item_view:
            default:
                my_value:
                    template: "@App/item/view/my_value.html.twig"
                    match:
                        item\value_type: my_value