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 object which comes from your CMS. For example, in eZ Platform integration, Netgen Layouts supports two types of values: eZ location and eZ 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 builder

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_block_manager:
    items:
        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

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\BlockManager\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 throw an exception 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 throw an exception 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

namespace AppBundle\Item\ValueLoader;

use Netgen\BlockManager\Item\ValueLoaderInterface;

class MyValueTypeLoader implements ValueLoaderInterface
{
    /**
     * Loads the value from provided ID.
     *
     * @param int|string $id
     *
     * @throws \Netgen\BlockManager\Exception\Item\ItemException If value cannot be loaded
     *
     * @return mixed
     */
    public function load($id)
    {
        try {
            return $this->myBackend->loadMyObject($id);
        } catch (Exception $e) {
            throw new ItemException(
                sprintf('Object with ID "%s" could not be loaded.', $id),
                0,
                $e
            );
        }
    }

    /**
     * Loads the value from provided remote ID.
     *
     * @param int|string $remoteId
     *
     * @throws \Netgen\BlockManager\Exception\Item\ItemException If value cannot be loaded
     *
     * @return mixed
     */
    public function loadByRemoteId($remoteId)
    {
        try {
            return $this->myBackend->loadMyObjectByRemoteId($remoteId);
        } catch (Exception $e) {
            throw new ItemException(
                sprintf('Object with remote ID "%s" could not be loaded.', $remoteId),
                0,
                $e
            );
        }
    }
}

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

app.block_manager.value_loader.my_value_type:
     class: AppBundle\Item\ValueLoader\MyValueTypeLoader
     tags:
         - { name: netgen_block_manager.item.value_loader, value_type: my_value_type }

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

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\BlockManager\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 value objects in the same value converter. For example, in eZ Platform, 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

namespace AppBundle\Item\ValueConverter;

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

class MyValueTypeConverter implements ValueConverterInterface
{
    /**
     * Returns if the converter supports the object.
     *
     * @param mixed $object
     *
     * @return bool
     */
    public function supports($object)
    {
        return $object instanceof MyValue;
    }

    /**
     * Returns the value type for this object.
     *
     * @param mixed $object
     *
     * @return string
     */
    public function getValueType($object)
    {
        return 'my_value_type';
    }

    /**
     * Returns the object ID.
     *
     * @param \App\MyValue $object
     *
     * @return int|string
     */
    public function getId($object)
    {
        return $object->id;
    }

    /**
     * Returns the object remote ID.
     *
     * @param \App\MyValue $object
     *
     * @return int|string
     */
    public function getRemoteId($object)
    {
        return $object->remoteId;
    }

    /**
     * Returns the object name.
     *
     * @param \App\MyValue $object
     *
     * @return string
     */
    public function getName($object)
    {
        return $object->name;
    }

    /**
     * Returns if the object is visible.
     *
     * @param \App\MyValue $object
     *
     * @return bool
     */
    public function getIsVisible($object)
    {
        return $object->isVisible();
    }

    /**
     * Returns the object itself.
     *
     * This method can be used to enrich the object before it being rendered.
     *
     * @param \App\MyValue $object
     *
     * @return \App\MyValue
     */
    public function getObject($object)
    {
        $object->param = 'value';

        return $object;
    }
}

Once implemented, you need to register the converter in Symfony DI container and tag it with netgen_block_manager.item.value_converter tag:

app.block_manager.value_converter.my_value_type_content:
     class: AppBundle\Item\ValueConverter\MyValueTypeConverter
     tags:
         - { name: netgen_block_manager.item.value_converter }

Implementing a value URL builder

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

Note

getUrl 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

namespace AppBundle\Item\ValueUrlBuilder;

use Netgen\BlockManager\Item\ValueUrlBuilderInterface;

class MyValueTypeUrlBuilder implements ValueUrlBuilderInterface
{
    /**
     * Returns the object URL. Take note that this is not a slug,
     * but a full path, i.e. starting with /.
     *
     * @param mixed $object
     *
     * @return string
     */
    public function getUrl($object)
    {
        return $this->router->generate(
            'my_custom_route',
            array(
                'id' => $object->id,
            )
        );
    }
}

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

app.block_manager.value_url_builder.my_value_type:
     class: AppBundle\Item\ValueUrlBuilder\MyValueTypeUrlBuilder
     tags:
         - { name: netgen_block_manager.item.value_url_builder, value_type: my_value_type }

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

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 (Block Manager app) and one for frontend.

Implementing a backend template

A backend template, or rather, template for Block Manager 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="{{ ngbm_item_path(item) }}" 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 builders 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_block_manager:
    view:
        item_view:
            api:
                my_value:
                    template: "@App/api/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_block_manager:
    view:
        item_view:
            default:
                my_value:
                    template: "@App/item/view/my_value.html.twig"
                    match:
                        item\value_type: my_value