---
title: "Filter fields"
manual: "TYPO3 EXT:thuecat"
version: "main"
permalink: "https://docs.typo3.org/permalink/werkraummedia/thuecat:developers-filter-fields@main"
source: "Developers/FilterFields.rst"
rendered: "2026-10-09T11:47:03+00:00"
---

# Filter fields {#developers-filter-fields}

The search-and-filter form of tourist attractions (see [Frontend output](https://docs.typo3.org/permalink/werkraummedia/thuecat:frontend-output@main)) is built from filter
fields. This page covers how to add one and how the form resolves its context.

## Registering a filter field {#frontend-output-filter-fields}

Which filters the search mask offers is not a list in the controller. A filter field is a tagged
service implementing `\WerkraumMedia\ThueCat\Service\FilterField\FilterFieldDefinition`, tagged
`search.filter.field`. `\WerkraumMedia\ThueCat\Service\SearchFilterOptionsService` discovers
every implementation through that tag and builds it. Another extension adds a filter without editing
any class here.

Two storage shapes are supported, each with an abstract base to extend. Pick the one matching where
the values live. Both live in `Classes/Service/FilterField/`; the examples are the shipped
fields, in the same directory.

Values in a comma-separated uid column on the record itself, i.e. a TCA select without `MM` — base
`\WerkraumMedia\ThueCat\Service\FilterField\CommaColumnField`:

```php
<?php

namespace Vendor\Extension\Service\FilterField;

use WerkraumMedia\ThueCat\Service\FilterField\CommaColumnField;

final class TownFilterField extends CommaColumnField
{
    public function __construct()
    {
        parent::__construct(
            name: 'towns',
            recordColumn: 'town',
            optionTable: 'tx_thuecat_town',
        );
    }
}
```

Values in an MM table over a parent/child option table, offered as the tree below a configured
anchor — base `\WerkraumMedia\ThueCat\Service\FilterField\HierarchicalMmField`:

```php
<?php

namespace Vendor\Extension\Service\FilterField;

use WerkraumMedia\ThueCat\Import\Settings\CategoryAnchorSetting;
use WerkraumMedia\ThueCat\Service\FilterField\HierarchicalMmField;

final class CategoryFilterField extends HierarchicalMmField
{
    public function __construct()
    {
        parent::__construct(
            name: 'categories',
            mmTable: 'sys_category_record_mm',
            mmFieldName: 'categories',
            optionTable: 'sys_category',
            parentColumn: 'parent',
            anchorSetting: CategoryAnchorSetting::CategoryParent,
        );
    }
}
```

With `autoconfigure` enabled the tag is applied by the interface, so no entry in
`Services.yaml` is needed.

### What each argument means {#what-each-argument-means}

-   **`name`**

    The demand property and the view key. The template binds the option set under this name, so a
    field named `towns` is rendered from `{towns}`. It is not the MM field name: the two
    happen to coincide today but answer to different owners.

-   **`recordColumn`**

    The column holding the comma-separated uids. A record without a value carries the placeholder
    `0`, which is never offered as an option.

-   **`mmFieldName`**

    Which field of a shared MM table the relation belongs to. Categories and keywords share
    `sys_category_record_mm` and are told apart by this alone, so it is part of every query.

-   **`anchorSetting`**

    A case of `\WerkraumMedia\ThueCat\Import\Settings\CategoryAnchorSetting`, naming the site
    setting that holds the category the offered tree starts below. The declaration carries the
    *setting*, never a resolved uid — anchors are per site and are resolved per request. An
    unconfigured anchor offers nothing.

The record table is deliberately absent. It belongs to the scope, not to the field, so one field
definition serves every record kind that offers it.

### What a field does not decide {#what-a-field-does-not-decide}

A field declares where its values are read from and nothing else. Scoping is resolved once per
request and applied to every field alike:

-   Only values carried by records the sibling list can return are offered. With no list on the page,
    each field offers what its storage holds.
-   Values are bound to the site. A value living outside the list's storage pages is still offered;
    one belonging to another site is not.
-   Option sets never depend on the visitor's current selection. Choosing one town does not remove the
    others from the form.

### Adding a further storage shape {#adding-a-further-storage-shape}

If neither base fits, the shape needs its own provider: implement
`\WerkraumMedia\ThueCat\Service\FilterField\OptionProvider\FilterOptionProvider`, tagged
`search.filter.option.provider`, returning `true` from `supports()` for the field class it
reads. Providers are discovered by that tag, so the service picks one up without being edited.

Extending `\WerkraumMedia\ThueCat\Service\FilterField\OptionProvider\AbstractOptionProvider`
supplies the restriction handling every option query needs — deleted, disabled, language, storage
pages and the site bound — which is easy to get subtly wrong when writing a provider from scratch.
The shipped providers, `CommaColumnOptionProvider` and `HierarchicalOptionProvider`, sit
in the same namespace as worked examples.

## How the search form finds its list {#how-the-search-form-finds-its-list}

The search-and-filter form does not read its own settings to learn the preset. A resolver inspects
the current page for a list content element, reads its FlexForm, and returns the active preset:

-   No list on the page -- the form targets the configured search result page.
-   A list on the page -- the form stays on the page; a filtered list additionally contributes the
    preselected fields.

The lookup is language- and overlay-aware, because it reads the stored content element through the
frontend's record retrieval.

The form is rendered by `TouristAttractionSearch` but submits under `TouristAttractionList`, so
the demand travels in the list's namespace. On re-render the form adopts that demand to re-populate
its fields, and the editor preset is forced onto the locked fields, so the hidden inputs always
carry the editor's values.
