Custom Predicates
Predicates are small, reusable query-fragment builders. They are the only place in FLARE where filter conditions are written into the SQL query — filter elements never touch the query directly; they translate their configuration and the filter's value into calls to predicates.
v0.2 renamed the query-side "filter type" to predicate (Filter\Type\ → Filter\Predicate\,
FilterTypeInterface → PredicateInterface, buildQuery() → buildConditions()). The word "type" now only
means the registered alias of a filter element or list driver.
Because predicates are stateless services with option-validated inputs, the same predicate can be reused by multiple filter elements or invoked from a programmatic filter.
For the predicates that ship with FLARE, see the built-in predicates reference.
1. The Interface
namespace HeimrichHannot\FlareBundle\Filter\Predicate;
#[AutoconfigureTag(self::FLARE_FILTER_PREDICATE_TAG)]
interface PredicateInterface
{
public const FLARE_FILTER_PREDICATE_TAG = 'flare.filter_predicate';
public function configureOptions(OptionsResolver $resolver): void;
/**
* @param array<string, mixed> $options
*/
public function buildConditions(FilterConditionsBuilder $builder, array $options): void;
}
configureOptions()declares the predicate's option schema with Symfony's OptionsResolver. Every call to the predicate is resolved against this schema beforebuildConditions()runs, so invalid options fail loudly and early.buildConditions()writes the conditions into theFilterConditionsBuilder, which is already scoped to the correct table alias.
Extend AbstractPredicate if your predicate has no options — it provides a no-op configureOptions().
Registration
There is no attribute to add: the interface carries #[AutoconfigureTag('flare.filter_predicate')], so any
service implementing it is registered automatically when autoconfiguration is enabled (the default). If you
configure services manually, tag them with flare.filter_predicate. Predicates are looked up by class
name in the FilterPredicateRegistry.
2. The FilterConditionsBuilder
The FilterConditionsBuilder (HeimrichHannot\FlareBundle\Query\FilterConditionsBuilder) provides a safe and
fluent API for building filter conditions. It enforces parameterized queries and handles table aliasing
automatically.
Key Methods:
column(string $column): Returns the quoted column name prefixed with the correct table alias (e.g.,`main`.`my_field`). Always use this for column names!where(string|CompositeExpression $query, ?array $params = null): Adds a WHERE condition.whereAnd(...)/whereOr(...): Combine multiple conditions.expr(): Returns Doctrine'sExpressionBuilderfor composing conditions (eq(),gt(),like(), ...).setParameter(string $param, mixed $value): Safely binds a value to a placeholder.whereInSerialized(array|int|string $find, string $column): Special helper for filtering against Contao's serialized array columns.abort(): Static method to immediately stop filtering and return an empty result set (e.g., if a required value is missing).
3. Writing a Custom Predicate
namespace App\Flare\Predicate;
use HeimrichHannot\FlareBundle\Filter\Predicate\AbstractPredicate;
use HeimrichHannot\FlareBundle\Query\FilterConditionsBuilder;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MinPricePredicate extends AbstractPredicate
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->define('field')->required()->allowedTypes('string');
$resolver->define('min')->required()->allowedTypes('int', 'float');
}
public function buildConditions(FilterConditionsBuilder $builder, array $options): void
{
$builder->where($builder->expr()->gte($builder->column($options['field']), ':min'))
->setParameter('min', $options['min']);
}
}
That's it — no attribute, no manual registration.
4. Consuming Predicates
From a filter element, inside buildContext():
public function buildContext(
FilterContextBuilderInterface $builder,
ElementScope $scope,
?ValueInterface $value,
): void {
if (!$value = $this->expectValue($value, MyPriceValue::class)) {
return; // no value, no condition
}
$builder->addPredicate(MinPricePredicate::class, [
'field' => $scope->get('field'),
'min' => $value->min,
]);
}
addPredicate() takes the predicate class, its options and an optional target alias. The options are always
validated against the predicate's configureOptions() schema.
Programmatically, through a
programmatic filter whose element
emits the call — added to a list through ListSpecBuilder::addFilter() or ListSpec::withFilter():
use HeimrichHannot\FlareBundle\Filter\Element\FilterElementInterface;
use HeimrichHannot\FlareBundle\Filter\Filter;
use HeimrichHannot\FlareBundle\Config\ComponentConfig;
use HeimrichHannot\FlareBundle\Filter\ElementBinding;
use HeimrichHannot\FlareBundle\Filter\ElementScope;
use HeimrichHannot\FlareBundle\Filter\FilterContextBuilderInterface;
use HeimrichHannot\FlareBundle\Filter\Value\ValueInterface;
$element = new class implements FilterElementInterface {
public function buildContext(
FilterContextBuilderInterface $builder,
ElementScope $scope,
?ValueInterface $value,
): void {
$builder->addPredicate(MinPricePredicate::class, ['field' => 'price', 'min' => 10]);
}
};
$filter = new Filter(
// the type is only used for named events; free for an unregistered element
element: new ElementBinding($element, 'app_min_price', new ComponentConfig()),
alias: 'min_price', // the filter's key within the list
);
From Twig, use engine mods — e.g. the built-in equation mod — to add filters to
a rendered list.