A backend widget for Contao that allows editors to search and select records from a foreign database table without loading the entire dataset.
It is specifically designed for large datasets where a traditional select, checkboxWizard, or static select field would be inefficient or unusable.
Install via Composer:
composer require terminal42/contao-tablelookupwizardPlease choose the appropriate major version based on your Contao version:
- 4.x → for Contao 5.6 and below
- 5.x → for Contao 5.7 and above
Make sure to select the correct major version to ensure compatibility with your Contao installation.
If your project needs to support both Contao 5.6 and 5.7+, you can allow both major
versions using a version union constraint in your composer.json:
"require": {
"terminal42/contao-tablelookupwizard": "^4.0 || ^5.0"
}Composer will then automatically install:
4.xwhen the project uses Contao ≤ 5.65.xwhen the project uses Contao ≥ 5.7
No code changes are needed on your end — the correct version will be resolved
based on the Contao version constraint in your own composer.json.
If you want to support both 4.x and 5.x simultaneously with a custom template,
you need to set customTpl dynamically at runtime rather than statically in the DCA,
since the template name changed between versions. For example, you can use an attributes callback
to detect the installed version and return the correct template name:
<?php
use Composer\InstalledVersions;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
#[AsCallback('tl_my_table', 'fields.my_field.attributes')]
class MyFieldAttributesListener
{
public function __invoke(array $attributes): array
{
$isNewVersion = version_compare(InstalledVersions::getVersion('terminal42/contao-tablelookupwizard'), '5.0', '>=');
if ($newNewVersion) {
$attributes['eval']['customTpl'] = 'backend/widget/custom_tablelookupwizard';
$attributes['eval']['customRecordsTpl'] = 'backend/widget/custom_tablelookupwizard_records';
} else {
$attributes['eval']['customTpl'] = 'be_widget_tablelookupwizard_content_custom';
$attributes['eval']['customContentTpl'] = 'be_widget_tablelookupwizard_content_custom';
}
return $attributes;
}
}Several DCA configuration properties were renamed or replaced in 5.x. The table below lists all breaking changes.
| Version 4.x | Version 5.x | Notes |
|---|---|---|
fieldType |
multiple |
checkbox → multiple: true, radio → multiple: false |
customLabels |
headerFields |
- |
matchAllKeywords |
searchMatchAll |
- |
enableSorting |
isSortable |
Also requires multiple: true to be effective |
joins |
sqlJoins |
Inner keys also renamed, see below |
joins.[table].jkey |
sqlJoins.[table].joinKey |
- |
joins.[table].fkey |
sqlJoins.[table].foreignKey |
- |
customContentTpl |
customRecordsTpl |
- |
The default template names have also been renamed. If you have overridden any of the templates in your project, update them accordingly:
| Version 4.x (.html5) | Version 5.x (.html.twig) |
|---|---|
be_widget_tablelookupwizard |
backend/widget/tablelookupwizard |
be_widget_tablelookupwizard_content |
backend/widget/tablelookupwizard_records |
'myField' => [
'inputType' => 'tableLookup',
'eval' => [
'foreignTable' => 'tl_news',
'listFields' => ['headline'],
],
'sql' => ['type' => \Doctrine\DBAL\Types\Types::INTEGER, 'unsigned' => true, 'default' => 0],
],| Option | Type | Default | Description |
|---|---|---|---|
foreignTable |
string |
- | Base database table used for querying records. |
listFields |
string[] |
- | Fields displayed in the result table. Use fully qualified names when using joins. |
| Option | Type | Default | Description |
|---|---|---|---|
multiple |
bool |
false |
Allows selecting multiple records. |
isSortable |
bool |
false |
Enables drag & drop sorting. Only effective when multiple = true. |
headerFields |
string[] |
[] |
Custom column header labels. Must match listFields length. |
| Option | Type | Default | Description |
|---|---|---|---|
searchFields |
string[] |
[] |
Fields used for keyword searching. Defaults to listFields. |
searchMatchAll |
bool |
false |
If true, all keywords must match (AND). Otherwise OR logic is used. |
searchLabel |
string |
"Search" |
Label for the search button. |
| Option | Type | Default | Description |
|---|---|---|---|
customTpl |
string |
backend/widget/tablelookupwizard |
Custom backend widget template. |
customRecordsTpl |
string |
backend/widget/tablelookupwizard_records |
Template for rendering selected records. |
| Option | Type | Default | Description |
|---|---|---|---|
sqlWhere |
string |
'' |
Additional SQL condition appended to WHERE (without the WHERE keyword). |
sqlOrderBy |
string |
'' |
Adds ORDER BY clause to search results. |
sqlGroupBy |
string |
'' |
Adds GROUP BY clause. |
sqlLimit |
int |
30 |
Maximum number of result rows displayed. |
sqlJoins |
array |
[] |
Defines SQL joins to make related tables available. |
| Parameter | Type | Description |
|---|---|---|
type |
string |
SQL join type (INNER JOIN, LEFT JOIN, RIGHT JOIN). |
joinKey |
string |
Column on the base table (foreignTable). |
foreignKey |
string |
Column on the joined table. |
After joining, fields can be referenced as:
tl_news_archive.title
'myField' => [
'inputType' => 'tableLookup',
'eval' => [
// Required
'foreignTable' => 'tl_news',
'listFields' => [
'tl_news.headline',
'tl_news.date',
'tl_news_archive.title',
],
// Selection behavior
'multiple' => true,
'isSortable' => true,
'headerFields' => [
'Headline',
'Date published',
'News archive',
],
// Search behavior
'searchFields' => [
'tl_news.headline',
'tl_news_archive.title',
],
'searchMatchAll' => true,
'searchLabel' => 'Search records',
// Template overrides
'customTpl' => 'backend/widget/custom_tablelookupwizard',
'customRecordsTpl' => 'backend/widget/custom_tablelookupwizard_records',
// SQL customization
'sqlWhere' => 'tl_news.protected=1',
'sqlOrderBy' => 'tl_news.date DESC',
'sqlGroupBy' => 'tl_news.pid',
'sqlLimit' => 100,
'sqlJoins' => [
'tl_news_archive' => [
'type' => 'INNER JOIN',
'joinKey' => 'pid',
'foreignKey' => 'id',
],
],
],
'sql' => ['type' => \Doctrine\DBAL\Types\Types::BLOB, 'notnull' => false],
],Storage behavior:
multiple => false→ single ID storedmultiple => true→ serialized array of IDs stored
The DCA field should use:
// multiple => false
'sql' => ['type' => \Doctrine\DBAL\Types\Types::INTEGER, 'unsigned' => true, 'default' => 0],
// multiple => true
'sql' => ['type' => \Doctrine\DBAL\Types\Types::BLOB, 'notnull' => false],Adjust if your implementation differs.
- Use fully qualified column names when joins are present.
- Ensure
headerFieldscount matcheslistFields. - Avoid unsanitized input inside
sqlWhere. - Index searchable fields for performance.