Custom List Drivers
List drivers define the source of data for a list and how the base query should be structured. A driver
class is registered under a type alias — the value stored in tl_flare_list.type when the list is
configured in the backend.
1. Registration (#[AsListDriver])
To create a custom list driver, annotate your class with the #[AsListDriver] attribute. Your class should ideally extend AbstractListDriver to inherit default behavior.
use HeimrichHannot\FlareBundle\DependencyInjection\Attribute\AsListDriver;
use HeimrichHannot\FlareBundle\List\Driver\AbstractListDriver;
#[AsListDriver(
type: 'my_custom_list',
dataContainer: 'tl_my_table'
)]
class MyCustomListDriver extends AbstractListDriver
{
// ...
}
Attribute Parameters:
type: Unique identifier for the list type.dataContainer: (Optional) Backend default for the record'sdcfield — the main database table (e.g.,tl_news). See Data Container Resolution.
The attribute is repeatable: a driver class annotated multiple times is registered once per type alias.
#[AsListType] was renamed to #[AsListDriver], and AbstractListType to AbstractListDriver
(HeimrichHannot\FlareBundle\List\Driver\). The registered type aliases are unchanged, so existing
database records keep working. The palette parameter was removed — backend palettes are now built in
buildDca().
2. Config Schema (configureOptions + configureTransformers)
A list's canonical config is resolved against two schemas: the framework-owned base schema
(HeimrichHannot\FlareBundle\List\BaseListOptions, covering the tl_flare_list columns — data
container (dc), title, jump-to pages, sorting, parent-table settings, meta formats, ...) and the
driver schema your driver declares on top in configureOptions():
use Symfony\Component\OptionsResolver\OptionsResolver;
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->define('archives')->default([])->allowedTypes('int[]');
}
configureTransformers() declares how stored sources translate onto that schema. AbstractListDriver
already registers a transformer for ListModel (a stored tl_flare_list record) that delegates to the
protected transformListModel() hook — the base columns are translated by BaseListOptions beforehand,
so the hook only maps the driver's own fields. All deserialization, casting, and enum parsing belongs
there:
use Contao\StringUtil;
use HeimrichHannot\FlareBundle\Config\ConfigBuilder;
use HeimrichHannot\FlareBundle\Model\ListModel;
protected function transformListModel(ConfigBuilder $config, ListModel $model): void
{
$config->set('archives', \array_map('\intval', StringUtil::deserialize($model->archives, true)));
}
Both methods are declarative, memoizable setup — they run once per driver class, not per list.
Downstream, the resolved config is available as ListSpec::$config.
3. Data Container Resolution (getDataContainerName)
Every ListSpec has a definitive main table. When the spec is created, the driver's
getDataContainerName(array $config): string (declared on ListDriverInterface) is called with the
resolved canonical config; an empty return value aborts the spec creation with an exception.
AbstractListDriver returns $config['dc'] — the record's stored dc column, translated by the base
schema — which is right for drivers whose table is user-selected.
A driver pinned to one table overrides it (and typically also implements the backend-side
DataContainerContract, which keeps the stored dc column
in sync while the record is edited):
public function getDataContainerName(array $config): string
{
return 'tl_news';
}
The attribute's dataContainer parameter is only a backend default for new records — it plays no
part in runtime resolution.
4. Build Lifecycle (buildList)
A list is built by the ListSpecBuilder into an immutable
ListSpec. Drivers that implement
Contract\ListDriver\BuildListContract take part in that lifecycle: buildList() runs first and may add
filters or set config overrides. Afterwards, the ListBuildEvent
(flare.list.{type}.build) gives third parties the same access, then the config is assembled and
resolved.
The built-in news list driver uses this to guarantee a published-state filter:
use HeimrichHannot\FlareBundle\Filter\Element\PublishedFilterElement;
use HeimrichHannot\FlareBundle\Filter\Factory\FilterFactory;
use HeimrichHannot\FlareBundle\List\ListSpecBuilder;
class MyCustomListDriver extends AbstractListDriver
{
public function __construct(
private readonly FilterFactory $filterFactory,
) {}
public function buildList(ListSpecBuilder $builder): void
{
if ($builder->hasFilterInstance(PublishedFilterElement::class)) {
return;
}
$builder->addFilter($this->filterFactory->create(
element: PublishedFilterElement::TYPE,
config: [
'intrinsic' => true,
'published_field' => 'published',
'start_field' => 'start',
'stop_field' => 'stop',
'invert' => false,
],
));
}
}
Besides addFilter()/removeFilter(), the builder exposes set(key, value) for canonical config
overrides (they win over the base translation and the driver's transformers) and read access to the
driver (getDriver()), the stored model (getModel()), the pre-resolution data container hint
(getDc()), and the already-added filters.
5. Building the Query (BuildQueryContract)
To customize the SQL query, implement Contract\ListDriver\BuildQueryContract (included in
AbstractListDriver with no-op defaults).
buildTableRegistry
Use this method to register joins.
use HeimrichHannot\FlareBundle\Query\JoinTypeEnum;
use HeimrichHannot\FlareBundle\Query\SqlJoinStruct;
use HeimrichHannot\FlareBundle\Query\TableAliasRegistry;
public function buildTableRegistry(TableAliasRegistry $registry): void
{
$registry->registerJoin(new SqlJoinStruct(
fromAlias: TableAliasRegistry::ALIAS_MAIN,
joinType: JoinTypeEnum::LEFT,
table: 'tl_member',
joinAlias: 'author',
condition: $registry->makeJoinOn('author', 'id', TableAliasRegistry::ALIAS_MAIN, 'author')
));
}
buildBaseQuery
Use this method to define base WHERE conditions, SELECT fields, or ORDER BY defaults.
use HeimrichHannot\FlareBundle\Query\SqlQueryStruct;
public function buildBaseQuery(SqlQueryStruct $struct): void
{
// Ensure only published items are shown by default
$struct->setConditions('main.published = "1"');
// Add custom select fields from the joined table
$select = $struct->getSelect() ?? [];
$select[] = 'author.username AS author_name';
$struct->setSelect($select);
}
6. Backend Configuration (buildDca)
If your list driver requires specific configuration fields in the Contao backend, implement
Contract\DcaContract and declare the palette — the legends and fields to display — in buildDca():
use HeimrichHannot\FlareBundle\DataContainer\Builder\DcaBuilder;
use HeimrichHannot\FlareBundle\DataContainer\Builder\DcaContext;
class MyCustomListDriver extends AbstractListDriver
{
public function buildDca(DcaBuilder $dca, DcaContext $context): void
{
$dca->palette('{my_legend},myCustomField,anotherField');
}
}
The referenced fields must be defined in the tl_flare_list DCA (e.g., in your extension's
contao/dca/tl_flare_list.php). See the Backend DCA Building guide for the full
DcaBuilder API, including per-field tweaks and palette prefixes/suffixes.