Creating custom query types

When installed on pure Symfony, Netgen Layouts comes with no built in query types which means you can only use manual collections in your blocks.

Tip

If you install Netgen Layouts on eZ Platform, you will get a single query type that will be automatically used when switching a collection from manual to dynamic in blocks.

When you implement your own query types, you will be able to use dynamic collections, and if you implement more than one query type, you will be able to select which query type to use when switching to dynamic collection in a block.

To implement a query type, you need a bit of configuration and a PHP class that will handle custom functionalities of a query type. In the following examples, we will show creating a custom query type that will use eZ search engine to search for items by text.

Configuring a new query type

To register a new query type in Netgen Layouts, you will need the following configuration:

netgen_block_manager:
    query_types:
        my_search:
            name: 'My search'

This configuration specifies a new query type with my_search identifier and My search human readable name.

Creating a PHP service for a query type

Every query type needs a single PHP class that specifies the entire behaviour of a query type. This class needs to implement Netgen\BlockManager\Collection\QueryType\QueryTypeHandlerInterface interface which specifies a number of methods for you to implement.

Let’s create a basic query type handler class:

<?php

namespace AppBundle\Collection\QueryType\Handler;

use Netgen\BlockManager\API\Values\Collection\Query;
use Netgen\BlockManager\Collection\QueryType\QueryTypeHandlerInterface;
use Netgen\BlockManager\Parameters\ParameterBuilderInterface;

class MySearchHandler implements QueryTypeHandlerInterface
{
    /**
     * Builds the parameters by using provided parameter builder.
     *
     * @param \Netgen\BlockManager\Parameters\ParameterBuilderInterface $builder
     */
    public function buildParameters(ParameterBuilderInterface $builder)
    {
    }

    /**
     * Returns the values from the query.
     *
     * @param \Netgen\BlockManager\API\Values\Collection\Query $query
     * @param int $offset
     * @param int $limit
     *
     * @return mixed[]
     */
    public function getValues(Query $query, $offset = 0, $limit = null)
    {
    }

    /**
     * Returns the value count from the query.
     *
     * @param \Netgen\BlockManager\API\Values\Collection\Query $query
     *
     * @return int
     */
    public function getCount(Query $query)
    {
    }

    /**
     * Returns the limit internal to this query.
     *
     * @param \Netgen\BlockManager\API\Values\Collection\Query $query
     *
     * @return int
     */
    public function getInternalLimit(Query $query)
    {
    }

    /**
     * Returns if the provided query is dependent on a context, i.e. current request.
     *
     * @param \Netgen\BlockManager\API\Values\Collection\Query $query
     *
     * @return bool
     */
    public function isContextual(Query $query)
    {
    }
}

Specifying query type parameters

First method we will look at is buildParameters method. By using an object called parameter builder and adding parameter specifications to it, this method will specify which parameters your custom query type will have. Details on how the parameter builder works, what parameter types exist and how to implement custom parameter type are explained in dedicated chapter.

Let’s add a couple of custom parameters to our query type which will serve as an input for search text:

use Netgen\BlockManager\Parameters\ParameterType;

public function buildParameters(ParameterBuilderInterface $builder)
{
    $builder->add('search_text', ParameterType\TextType::class);
    $builder->add('limit', ParameterType\IntegerType::class);
}

Notice that we didn’t specify the human readable labels for the parameters. That’s because they are generated automatically via translation system. To create the correct labels for your query type parameters, you need to add one string to ngbm translation catalog for every parameter in your query type with the format query.<query_type>.<parameter_name> where query_type and parameter_name are placeholders that need to be replaced with correct values.

So, for our custom search query type, the translation file would look something like this:

query.my_search.search_text: 'Search text'
query.my_search.limit: 'Limit'

Fetching the items

Second method in our handler example above is called getValues. This method is used for fetching the items from a query.

This method needs to return the array of domain objects that will be automatically converted to block items.

Tip

In case of eZ Platform, query types can return the list of eZ ContentInfo or Location value objects.

/**
 * @const int
 */
const DEFAULT_LIMIT = 25;

/**
 * @var \eZ\Publish\API\Repository\SearchService
 */
protected $searchService;

public function __construct(SearchService $searchService)
{
    $this->searchService = $searchService;
}

public function getValues(Query $query, $offset = 0, $limit = null)
{
    $searchResult = $this->searchService->findLocations(
        $this->buildQuery($query)
    );

    return array_map(
        function (SearchHit $searchHit) {
            return $searchHit->valueObject;
        },
        $searchResult->searchHits
    );
}

public function getInternalLimit(Query $query)
{
    $limit = $query->getParameter('limit')->getValue();
    if (!is_int($limit)) {
        return self::DEFAULT_LIMIT;
    }

    return $limit >= 0 ? $limit : self::DEFAULT_LIMIT;
}

/**
 * Builds the query from current parameters.
 *
 * @param \Netgen\BlockManager\API\Values\Collection\Query $query
 * @param bool $buildCountQuery
 *
 * @return \eZ\Publish\API\Repository\Values\Content\LocationQuery
 */
protected function buildQuery(Query $query, $buildCountQuery = false)
{
    $locationQuery = new LocationQuery();

    $criteria = array(
        new Criterion\FullText($query->getParameter('search_text')->getValue()),
        new Criterion\Visibility(Criterion\Visibility::VISIBLE),
    );

    $locationQuery->filter = new Criterion\LogicalAnd($criteria);

    $locationQuery->limit = 0;
    if (!$buildCountQuery) {
        $locationQuery->limit = $this->getInternalLimit($query);
    }

    return $locationQuery;
}

As you can see, getValues method simply builds a location query for eZ search engine and returns the list of found eZ locations. Conversion to block items is handled automatically by Netgen Layouts.

Note

Notice that we didn’t use $offset and $limit parameters which were provided to getValues method. These parameters are provided as a placeholder for future updates and are currently unused by the system and will always be equal to their default values.

Fetching the item count

To retrieve the item count from the query type, we use the getCount method:

public function getCount(Query $query)
{
    $searchResult = $this->searchService->findLocations(
        $this->buildQuery($query, true)
    );

    return $searchResult->totalCount;
}

Contextual queries

Notice how we implemented above a method called getInternalLimit to calculate the correct limit which will be applied to eZ search query. While it can obviously be used by the query type handler itself, the purpose of this method is to signal to the system how much items the collection would have if it was ran. This information is used when displaying a block in Block Manager app whose collection is a contextual one.

A contextual query is a query which needs the current context (i.e. current request) to run. Think of a situation where you have a layout with a block which shows top 5 items from the category it is applied to. Contextual query removes the need to create five different layouts for five different categories just so you can change the parent category from which to fetch the items. Instead, in a contextual query, you will take the currently displayed item and use it as the parent, making it possible to have only one layout for all five different categories.

In order for the system to work properly with contextual queries, two methods are used:

  • isContextual method, which signals to the system if the query is contextual or not. Most of the time, this method will return a value of a boolean parameter specified inside of the query which decides if a query is contextual or not, for example:

    public function isContextual(Query $query)
    {
        return $query->getParameter('use_current_location')->getValue() === true;
    }
    
  • already described getInternalLimit method, used by the block with a contextual query so it can display a preview of how the block would look like.

In our case, we will simply return false from isContextual method:

public function isContextual(Query $query)
{
    return false;
}

Defining the Symfony service for our handler

To connect the created handler with query type configuration, we need to register the handler in Symfony DIC:

services:
    app.collection.query_type.handler.my_search:
        class: AppBundle\Collection\QueryType\Handler\MySearchHandler
        arguments:
            - "@ezpublish.api.service.search"
        tags:
            - { name: netgen_block_manager.collection.query_type_handler, type: my_search }

This configuration is a fairly regular specification of services in Symfony, however, to correctly recognize our PHP class as a query type handler, we need to tag it with netgen_block_manager.collection.query_type_handler tag and attach to it an type key with a value which equals to the identifier of query type we configured at the beginning (in this case my_search).

After this, our query type is ready for usage.