Creating custom target types

Netgen Layouts is shipped with a number of target types to which you can map your layouts to be used in a layout resolving process. These target types are generic, allowing you to attach a layout to a request URI, a Symfony route or their prefixes. In most cases, when working with pure Symfony apps, this is enough.

However, if you wish that your layouts can be mapped to domain objects from your CMS directly, you can create custom target types.


Custom target type can be whatever comes to mind, not only a domain object from your CMS.


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

Implementing the target type classes

Implementing a target type in PHP requires creating at least three Symfony services:

  • Target type itself
  • Symfony form mapper
  • A target handler for every database engine supported in Netgen Layouts

Creating a target type

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

  • Provide Symfony constraints that validate the value of the target when adding it to a mapping
  • Extract the value of the target from the request to be used in layout resolving process
  • Making sure that value of a target 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 location target type, these constraints validate that the ID of the location is a number larger than 0 and that the location with the provided ID actually exists:

public function getConstraints(): array
    return [
        new Constraints\NotBlank(),
        new Constraints\Type(['type' => 'numeric']),
        new Constraints\GreaterThan(['value' => 0]),
        new IbexaConstraints\Location(),

The second point is achieved by implementing the provideValue method. This method takes a request object and should return a value of your target type if it exists in the request or null if it doesn’t. For example, Ibexa location target type extracts the location from provided request and returns its ID:

public function provideValue(Request $request)
    $location = $this->contentExtractor->extractLocation($request);

    return $location instanceof APILocation ? $location->id : null;

The third point is achieved by implementing export and import methods. E.g. in order for your targets 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 target type.

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

    class: App\Layout\Resolver\TargetType\MyTarget
        - { name: netgen_layouts.target_type }


You can add a priority attribute to the tag, which allows you to make your target type considered before others when deciding if the current request matches one of the targets.

Creating the form mapper

To be able to add the target to a mapping or edit the value of an existing target, you need to provide a form mapper which provides data for generating Symfony form for your target type. The mapper needs to implement Netgen\Layouts\Layout\Resolver\Form\TargetType\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 target:



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

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

final class MyTarget 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 target type:

    class: App\Layout\Resolver\Form\TargetType\Mapper\MyTarget
        - { name: netgen_layouts.target_type.form_mapper, target_type: my_target }

Creating target handlers for the database engine

Matching the target value from the request to the value stored in the database is done in the database itself. This means that you need to provide a so called target handler for every database engine supported in Netgen Layouts.

The only supported database engine is called “doctrine”, since it uses Doctrine library to communicate with the database.

This target handler needs to implement Netgen\Layouts\Persistence\Doctrine\QueryHandler\TargetHandlerInterface interface which provides a single method called handleQuery which takes the Doctrine query object and the target value and should modify the query in way to match the provided value.

Stored target value can be accessed in the query with rt.value so to match a simple integer, you would implement it like this:

public function handleQuery(QueryBuilder $query, $value): void
        $query->expr()->in('rt.value', [':target_value']),
    ->setParameter('target_value', $value, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY);

Finally, the target handler needs to registered in the Symfony container with the correct tag and target type identifier:

    class: App\LayoutResolver\TargetHandler\Doctrine\MyTarget
        - { name: netgen_layouts.target_type.doctrine_handler, target_type: my_target }

Implementing the target type template

Target type uses a single template in the value view context of the Netgen Layouts view layer to display the value of the target in the admin interface. Since the target itself usually provides only the scalar identifier as its value, this template usually needs some logic to display the name of the target (from your CMS for example). In case of Ibexa CMS, these templates for example use Twig functions to load the content and location objects and return their names and paths:

{{ nglayouts_ibexa_content_name(target.value) ?? '(INVALID CONTENT)' }}

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

                    template: "@App/layout_resolver/target/value/my_target.html.twig"
                        rule_target\type: my_target

Target type translations

Each target type uses two translation strings, one in nglayouts and one in nglayouts_admin catalog. The first one is a generic string which should provide a human readable name of the target type and should be in the<target_type_identifier> format:

The second one is used as a label in administration of interface which states for which target types is the mapping used and should be in layout_resolver.rule.target_header.<target_type_identifier> format: