Skip to main content
Version: v0.2 (next)

Migrating from v0.1

v0.2 rebuilds the filter subsystem around an element-owned architecture: a filter element now owns its config schema, backend DCA, frontend form, and query translation — and delegates the actual SQL to reusable filter types. The invoker and DCA-callback subsystems are gone.

If you only configure lists and filters in the Contao backend, nothing changes for you. DCA fields, palettes, backend filter labels, templates, and the content elements are unchanged. This guide is for developers who extended FLARE in PHP.

For the exhaustive list of deleted APIs, see Removed in v0.2.

Namespaces & Classes

v0.1v0.2
HeimrichHannot\FlareBundle\FilterElement\FooElementHeimrichHannot\FlareBundle\Filter\Element\FooFilterElement — all elements moved and renamed with a FilterElement suffix; the ::TYPE strings (e.g. flare_bool) are unchanged
Specification\FilterDefinition, Collection\FilterDefinitionCollectionFilter\Filter — a single immutable value object; see Filter Pipeline
FilterCollector\*Filter\Collector\*; collect() now returns array<string|int, Filter>|null
Form\Type\DateRangeFilterType (Symfony form type)Form\Type\DateRangeFormType — renamed; note that Filter\Type\DateRangeFilterType now names a query filter type instead
ListType\ListTypeInterface, ListType\AbstractListTypeList\Driver\ListDriverInterface, List\Driver\AbstractListDriver — "list types" are implemented by list drivers; the registered type aliases (e.g. flare_news) are unchanged
ListType\NewsListType, ListType\GenericDataContainerListType, ...List\Driver\NewsListDriver, List\Driver\GenericDataContainerListDriver, ... — built-in drivers renamed accordingly (see built-in list types)
Contract\ListType\*Contract\ListDriver\*; DataContainerContract::getDataContainerName() is now resolveDataContainerTable() — the old name lives on ListDriverInterface and resolves the runtime table from canonical config

Attributes

v0.1v0.2
#[AsFilterElement(type:, formType:, palette:, method:, isTargeted:)]#[AsFilterElement(type:, isTargeted:)] — form type moves into buildForm(), palette into buildDca(), the invoked method is always buildFilter(); always-intrinsic elements implement IntrinsicContract
#[AsListType(type:, dataContainer:, palette:)]#[AsListDriver(type:, dataContainer:)] — renamed; palette moves into buildDca(); dataContainer is only a backend default for new records
#[AsFilterInvoker]Removed — branch on $context->engineContext inside buildFilter()
#[AsFilterCallback], #[AsListCallback], #[AsFlareCallback]Removed — use buildDca() / ElementDcaEvent

Filter Element API

The __invoke(FilterInvocation $invocation, FilterQueryBuilder $qb) entry point is replaced by three lifecycle methods (see the custom filter elements guide):

v0.1v0.2
formType: attribute parameterbuildForm(FilterFormBuilderInterface $builder, FilterContext $context): void — declare one field via single() or add children under local names
__invoke(FilterInvocation, FilterQueryBuilder)buildFilter(FilterBuilderInterface $builder, FilterContext $context, array $values): void — emit filter-type calls instead of writing SQL
SQL written directly in the elementA FilterTypeInterface service; the element calls $builder->add(MyFilterType::class, [...])
FilterInvocation->getValue()$values (submitted form data by local child name; single() fields under FilterContext::SINGLE_VALUE) and $context->config (resolved config)
FilterQueryBuilder::abort() in the element$builder->abort() on the FilterBuilderInterface (filter types may still use FilterQueryBuilder::abort())

Contracts

v0.1 contractv0.2 equivalent
FormDataContract::extractFormData()Read $values in buildFilter() — it contains the filter's submitted data
RuntimeValueContract::processRuntimeValue()Normalize values inside buildFilter()
IntrinsicValueContract::getIntrinsicValue()Read $context->config in buildFilter() (intrinsic values are config)
HydrateFormContract::hydrateForm()Pass defaults via the fields' data option in buildForm()
FormTypeOptionsContract::handleFormTypeOptions()Build options directly in buildForm(); third parties use FilterElementFormBuiltEvent
PaletteContract::getPalette()DcaContract::buildDca()

New interfaces: FilterElementInterface (required), OptionsContract (config schema), TransformerContract (source-to-config translation), DcaContract (backend DCA), and IntrinsicContract (always-intrinsic marker). AbstractFilterElement implements all of them plus IsSupportedContract.

Events

v0.1v0.2
FilterElementInvokingEvent (flare.filter_element.{type}.invoking)FilterElementBuildingEvent (flare.filter_element.{type}.building) — cancellable via setShouldBuild(false)
FilterElementInvokedEvent (flare.filter_element.{type}.invoked)FilterElementBuiltEvent (flare.filter_element.{type}.built)
FilterFormChildOptionsEvent (flare.form.{parent}.child.{name}.options)Removed — customize in buildForm(), or listen to FilterElementFormBuiltEvent (flare.filter_element.{type}.form_built)
FilterElementFormTypeOptionsEventRemoved — same replacement as above
PaletteEvent (flare.filter_element.{alias}.palette, flare.list.{type}.palette)ElementDcaEvent (flare.filter_element.{type}.dca, flare.list.{type}.dca)
FilterDefinitionCreatedEventFilterCollectedEvent — dispatched per filter collected from the database; $event->filter is replaceable

flare.form.{formName}.build (FilterFormBuildEvent) is unchanged.

ListSpecification → ListSpec

ListSpecification is replaced by the immutable List\ListSpec, built by the List\ListSpecBuilder (see List Specs & Filters); programmatic construction goes through the ListSpecFactory service, which accepts a driver instance or a registered type alias and resolves the list's data container definitively. The spec carries the driver instance as $spec->driver; the main table is $spec->dc. Filters are a keyed array<string, Filter> instead of a FilterDefinitionCollection. While a list is being built (a driver's buildList() hook or a ListBuildEvent listener), use the mutable builder; on a finished spec, use the withers:

v0.1v0.2
$spec->getFilters()->add($definition)$builder->addFilter($filter) while building; $spec = $spec->withFilter($filter) afterwards
$spec->getFilters()->set('name', $definition)$builder->addFilter($filter, 'name') / $spec->withFilter($filter, 'name') — an existing key is replaced
$spec->getFilters()->hasType('flare_bool')hasFilterInstance(BooleanFilterElement::class) on builder or spec
$builder->getFilter('name'), $builder->removeFilter('name'), $spec->withoutFilter('name')
$definition->forceTargetAlias('alias')$filter->withTargetAlias('alias')returns a new instance (Filter is immutable)

Element define() factories (e.g. PublishedFilterElement::define(), SimpleEquationFilterElement::define(...)) were removed: construct a Filter directly with the element's type alias and canonical config, e.g. new Filter(element: PublishedFilterElement::TYPE, config: ['intrinsic' => true, ...]).

Before / After

A minimal v0.1 element and its v0.2 equivalent:

// v0.1
#[AsFilterElement(type: 'app_city', formType: TextType::class, palette: '{filter_legend},fieldGeneric')]
class CityElement extends AbstractFilterElement
{
public function __invoke(FilterInvocation $invocation, FilterQueryBuilder $qb): void
{
if (!$value = $invocation->getValue()) {
return;
}

$qb->where($qb->column('city') . ' = :city', ['city' => $value]);
}
}
// v0.2
#[AsFilterElement(type: 'app_city', isTargeted: true)]
class CityFilterElement extends AbstractFilterElement
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->define('field')->default('city')->allowedTypes('string');
}

protected function transformFilterModel(ConfigBuilder $config, FilterModel $model): void
{
$config->set('field', $model->fieldGeneric ?: 'city');
}

public function buildDca(DcaBuilder $dca, DcaContext $context): void
{
$dca->palette('{filter_legend},fieldGeneric');
}

public function buildForm(FilterFormBuilderInterface $builder, FilterContext $context): void
{
$builder->single(TextType::class, ['required' => false]);
}

public function buildFilter(FilterBuilderInterface $builder, FilterContext $context, array $values): void
{
if (!$value = $values[FilterContext::SINGLE_VALUE] ?? null) {
return;
}

$builder->add(SimpleEquationFilterType::class, [
'operand_left' => $context->config['field'],
'operator' => SqlEquationOperator::EQUALS,
'operand_right' => (string) $value,
]);
}
}