Creating custom condition types

Netgen Layouts is shipped with a couple of condition types which you can use to limit your layout mappings to certain conditions. One example condition type which is built into Netgen Layouts is the query parameter condition type which enables the mapping if some query parameter from the request matches the value stored in the condition. In case of Ibexa CMS integration, an example condition type which is built in is the siteaccess condition type that activates the mapping only if the siteaccess of the current request matches the condition.

If you wish to map your layouts to targets in some other conditions, like time of day, location of the user, IP address, you name it…, you can create your own condition types.

Warning

Custom condition types are one of the most complicated extension points in Netgen Layouts and creating a custom condition type involves creating configuration, templates, translations and quite a bit of code.

Implementing the condition type classes

Implementing a condition type in PHP requires creating two Symfony services:

  • Condition type itself

  • Symfony form mapper

Creating a condition type

A condition type is a PHP class implementing Netgen\Layouts\Layout\Resolver\ConditionTypeInterface (or extending Netgen\Layouts\Layout\Resolver\ConditionType abstract class to cut down on boilerplate). This class will have a couple of purposes related to a condition type:

  • Provide Symfony constraints that validate the value of the condition when adding it to a mapping

  • Deciding if the current request matches the provided value of the condition

  • Making sure that value of a condition is correctly preserved across systems (e.g. dev and prod) when exporting/importing mappings.

The first point is achieved by implementing getConstraints method, which should return the array of Symfony validator constraints which should validate the value. For example, in Ibexa siteaccess condition type, these constraints validate that all selected siteaccesses are non empty strings and that they actually exist:

public function getConstraints(): array
{
    return [
        new Constraints\NotBlank(),
        new Constraints\Type(['type' => 'array']),
        new Constraints\All(
            [
                'constraints' => [
                    new Constraints\Type(['type' => 'string']),
                    new IbexaConstraints\SiteAccess(),
                ],
            ],
        ),
    ];
}

The second point is achieved by implementing the matches method. This method takes a request object and based on the data from the request decides if it matches the provided value. For example, the matches method of the Ibexa siteaccess condition type returns true only if the siteaccess is provided in the request and is equal to one of the stored values of the condition:

public function matches(Request $request, $value): bool
{
    $siteAccess = $request->attributes->get('siteaccess');
    if (!$siteAccess instanceof SiteAccess) {
        return false;
    }

    if (!is_array($value) || count($value) === 0) {
        return false;
    }

    return in_array($siteAccess->name, $value, true);
}

The third point is achieved by implementing export and import methods. E.g. in order for your conditions to be correctly recognized in different systems, you would want to use some form of a remote IDs when exporting them, instead of using autogenerated database IDs. Example export and import methods would then look like this:

public function export($value)
{
    return $this->loadById($value)->remoteId;
}

public function import($value)
{
    return $this->loadByRemoteId($value)->id;
}

The one method that remains to be implemented is the getType method, which should return a unique identifier of the condition type.

Once this is done, we need to register the condition type in the Symfony DIC with the netgen_layouts.condition_type tag:

app.condition_type.my_condition:
    class: App\Layout\Resolver\ConditionType\MyCondition
    tags:
        - { name: netgen_layouts.condition_type }

Creating the form mapper

To be able to add the condition to a mapping or edit the value of an existing condition, you need to provide a form mapper which provides data for generating Symfony form for your condition type. The mapper needs to implement Netgen\Layouts\Layout\Resolver\Form\ConditionType\MapperInterface and there’s also a handy abstract class which you can extend to cut down the number of methods to define to one: getFormType, which returns which Symfony form type should be used to edit the condition:

<?php

declare(strict_types=1);

namespace App\Layout\Resolver\Form\ConditionType\Mapper;

use Netgen\Layouts\Layout\Resolver\Form\ConditionType\Mapper;
use Symfony\Component\Form\Extension\Core\Type\TextType;

final class MyCondition extends Mapper
{
    public function getFormType(): string
    {
        return TextType::class;
    }
}

There are two other methods in the interface:

  • getFormOptions which makes it possible to provide custom options to the form type

  • handleForm which allows you to customize the form in any way you see fit

Finally, you need to register the mapper in the Symfony container with the correct tag and the identifier of the condition type:

app.layout.resolver.form.condition_type.mapper.my_condition:
    class: App\Layout\Resolver\Form\ConditionType\Mapper\MyCondition
    tags:
        - { name: netgen_layouts.condition_type.form_mapper, condition_type: my_condition }

Implementing the condition type template

Condition type uses a single template in the value view context of the Netgen Layouts view layer to display the value of the condition in the admin interface. Since the condition itself usually provides only the scalar identifier as its value, this template usually needs some logic to display the human readable value of the condition. For example, content type condition from Ibexa CMS uses custom Twig functions to display content type names instead of the identifiers:

{% set content_type_names = [] %}

{% for value in condition.value %}
    {% set content_type_names = content_type_names|merge([nglayouts_ibexa_content_type_name(value)]) %}
{% endfor %}

{{ content_type_names|join(', ') }}

To register the template in the system, the following configuration is needed (make sure to use the value view context):

netgen_layouts:
    view:
        rule_condition_view:
            value:
                my_condition:
                    template: "@App/layout_resolver/condition/value/my_condition.html.twig"
                    match:
                        rule_condition\type: my_condition

Condition type translations

Each condition type uses one translation string in the nglayouts catalog. This is a generic string which should provide a human readable name of the condition type and should be in the layout_resolver.condition.<condition_type_identifier> format: