PHPackages                             oliverthiele/ot-recordselector - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. oliverthiele/ot-recordselector

ActiveTypo3-cms-extension[Utility &amp; Helpers](/categories/utility)

oliverthiele/ot-recordselector
==============================

Custom backend form element for selecting TYPO3 records with translated titles, AJAX autocomplete, permission checks, and hidden-record indicators

1.1.0(3mo ago)09GPL-2.0-or-laterPHP

Since Apr 2Pushed 1mo agoCompare

[ Source](https://github.com/oliverthiele/ot-recordselector)[ Packagist](https://packagist.org/packages/oliverthiele/ot-recordselector)[ Docs](https://github.com/oliverthiele/ot-recordselector)[ RSS](/packages/oliverthiele-ot-recordselector/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (4)Versions (5)Used By (0)

OT Record Selector — TYPO3 Backend Form Element
===============================================

[](#ot-record-selector--typo3-backend-form-element)

A custom backend form element for TYPO3 that lets editors search and select records by name — **showing translated titles and field values in the editor's own language**, with relevance ranking, configurable info fields, preview images, and TYPO3-native card display.

[![TYPO3](https://camo.githubusercontent.com/ff624ed071afbc7085dcd4f99f2358379f8284ba14ae6891eab075f69c55929f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5459504f332d31332e342d6f72616e67652e737667)](https://typo3.org/)[![Packagist Version](https://camo.githubusercontent.com/7a303886c33ab81bc3ac99946b56c6f1511d41263824c5d261f029b086fc5271/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6f6c69766572746869656c652f6f742d7265636f726473656c6563746f722e737667)](https://packagist.org/packages/oliverthiele/ot-recordselector)[![PHP](https://camo.githubusercontent.com/28f350681a3f63af78acf9c3d2c96356b1bc3ce3ce232688ba27800352ddf416/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f6f6c69766572746869656c652f6f742d7265636f726473656c6563746f722f7068702e737667)](https://php.net/)[![License](https://camo.githubusercontent.com/7ab2a2e7f9e31de2a39b4639331c4c67e0b028da151d6d96adc777e27b1c06ec/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6f6c69766572746869656c652f6f742d7265636f726473656c6563746f722e737667)](LICENSE)[![Changelog](https://camo.githubusercontent.com/6bc02a7bc61afc1cb3faaa53420df6d904b9940d7f3e2e11a463e1fdbb3cd52d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4368616e67656c6f672d4348414e47454c4f472e6d642d626c75652e737667)](CHANGELOG.md)

---

Why not just use `type=group` or `selectMultipleSideBySide`?
------------------------------------------------------------

[](#why-not-just-use-typegroup-or-selectmultiplesidebyside)

TYPO3 core offers several ways to link records. They share a common limitation: **results are sorted alphabetically with no relevance ranking**, and **records always appear in the default language** regardless of which language the editor is currently working in.

OT Record Selector takes a different approach:

- **Label-field ranking.** Results where the search term appears in the record title rank above results where it only appears in a secondary field. Consider an address database where searching for `"peter mill"` returns two results: "Peter Miller" (rank 0 — both words match in the name) and "Peter Cooper" (rank 1 — "peter" matches in the name, but "mill" only matches in his job title "Mill supervisor"). Without ranking, alphabetical order would mix these arbitrarily. With ranking, the better match always appears first.
- **Configurable info fields.** "Peter Miller" and "Peter Cooper" are both valid results — but the editor needs to pick the right one. Any TCA field can be shown as labeled metadata directly on the card: `City: London · Email: p.miller@example.com`. Labels are resolved from TCA, so editors see human-readable field names. In the example above, showing city and email makes it immediately clear which Peter is which.
- **Three-line info display.** The card and dropdown show up to three lines: (1) system info (UID, PID), (2) content fields in the editor's language, (3) content fields in the default language — only when they differ from line 2. This lets editors immediately see both the translated and the original value side by side for context.
- **Translated titles and field values in search and cards.** The element detects the backend user's preferred language and overlays the translated title and all configured `infoFields` — in both the AJAX search results and the selected card. A German editor always sees German content, regardless of which language the edited record belongs to. `type=group` and `selectMultipleSideBySide` always show the default-language title regardless of the editor's working language.
- **Cross-language search.** The AJAX search covers both default-language records and their translations so editors can search in their own language. A German editor searching for "Müller" will find a contact stored as "Mueller" via the German translation of the name field.
- **Preview images.** When a FAL image field is configured (`previewImage`), the element shows a 64×64 thumbnail instead of the TYPO3 record icon — useful for any domain where visual recognition matters (contacts with portrait photos, products with product images, etc.).
- **TYPO3-native appearance.** Selected records render as Bootstrap `.card` elements with the record icon or preview image, an edit link, and a remove button — indistinguishable from core backend UI.
- **Multi-word AND search.** Searching for `"peter mill"` returns only records that contain both words, across all configured search fields.

---

Features
--------

[](#features)

- **Language-aware display** — search results and selected cards show translated titles and field values based on the backend user's preferred language; always stores the default-language UID
- **Cross-language search** — AJAX search covers default-language records and their translations simultaneously
- **Label-first relevance ranking** — matches in the label field rank above matches in secondary fields
- **Three-line info display** — (1) UID/PID, (2) translated content fields, (3) default-language content fields (only when different)
- **Preview images** — configurable FAL field for 64×64 thumbnails; falls back to TYPO3 record icon
- **TYPO3-native card UI** — record icon or preview image, title, hidden badge, configurable info lines, edit link, remove button
- **Multi-word AND search** — each space-separated word must match; ORed across all search fields
- **Configurable search fields** — restrict AJAX search to specific indexed columns (`searchFields`); falls back to `ctrl.searchFields` from TCA, then to the label field
- **Configurable info fields** — show any TCA fields as labeled metadata (`uid`, `pid`, or any column name)
- **Result limit** — configurable per field (`maxResults`), hard cap at 200
- **Permission-aware** — respects TYPO3 backend user `tables_select` and `tables_modify` permissions; the edit button is hidden when the editor cannot modify the table
- **Inaccessible record protection** — cards for records on pages the editor cannot access display a `no access` badge; removing them requires confirmation via a TYPO3-native modal
- **Hidden record indicator** — shows a `hidden` badge (yellow) when all checked versions are hidden, or a `partially hidden` badge (grey) when only one side is hidden
- **Accessibility** — ARIA `role=combobox`, `aria-expanded`, `aria-activedescendant`, keyboard navigation (↑ ↓ Enter Escape)
- **Debug mode** — shows `[tablename]` and `[fieldname]` next to the element label (mirrors TYPO3 core behavior)
- **Single- and multi-select** — `maxitems=1` hides the search after selection; `maxitems>1` keeps it visible and stores a comma-separated list of UIDs

---

Requirements
------------

[](#requirements)

RequirementVersionTYPO313.4+PHP8.3+No additional dependencies. The element uses `@typo3/core/ajax/ajax-request.js` and `` from TYPO3 core.

---

Installation
------------

[](#installation)

```
composer require oliverthiele/ot-recordselector
```

Then run the TYPO3 setup:

```
vendor/bin/typo3 extension:setup -e ot_recordselector
# or via DDEV:
ddev typo3 extension:setup -e ot_recordselector
```

---

Configuration
-------------

[](#configuration)

Register the form element in your TCA column configuration:

```
'my_field' => [
    'label' => 'My Record',
    'config' => [
        'type' => 'user',
        'renderType' => 'otRecordSelector',
        'foreign_table' => 'tx_myext_domain_model_record',
        'minitems' => 0,
        'maxitems' => 1,
    ],
],
```

### All TCA options

[](#all-tca-options)

OptionTypeDefaultDescription`foreign_table``string`—**Required.** Target table name (must exist in TCA)`maxitems``int``1``1` = single select, hides search after selection; `>1` = multi-select, stores comma-separated UIDs`minitems``int``0`Minimum required selections (not yet validated client-side)`infoFields``string``uid`Comma-separated list of fields to show as labeled metadata. Use `uid` and `pid` as special keywords.`searchFields``string`—Comma-separated DB columns to search in. Falls back to `ctrl.searchFields` from TCA, then to the label field. Only whitelisted TCA columns are accepted.`maxResults``int``20`Maximum number of AJAX search results. Hard cap: 200.`previewImage``string`—FAL field on the foreign table whose first image is shown as a 64×64 thumbnail instead of the record icon.`allowRootLevel``bool``false`When `true`, non-admin editors can see records stored at `pid=0` (site root level). Admin users always have access regardless of this setting.`allowRemoveInaccessible``bool``true`When `true` (default), the remove button is shown for inaccessible records, but clicking it opens a TYPO3 confirmation modal warning that the selection cannot be restored. When `false`, the remove button is hidden entirely for inaccessible records.> **Naming convention:** All options added by this extension follow lowerCamelCase (`infoFields`, `searchFields`, `maxResults`, `previewImage`, `allowRootLevel`), consistent with newer TYPO3 core TCA options like `renderType`. The older core options `minitems`, `maxitems`, and `foreign_table` keep their original spelling.

### Example: address record selector

[](#example-address-record-selector)

```
'contact_address' => [
    'label' => 'Contact',
    'config' => [
        'type' => 'user',
        'renderType' => 'otRecordSelector',
        'foreign_table' => 'tt_address',
        'minitems' => 0,
        'maxitems' => 1,
        'infoFields' => 'uid,city,email',
        'searchFields' => 'first_name,last_name,email,company,title',
        'maxResults' => 30,
        'previewImage' => 'image',
    ],
],
```

The selected card will show three lines:

1. `ID: 42 · PID: 5 · /contacts/`
2. `Stadt: London · E-Mail: p.mueller@example.com` *(editor's language)*
3. `City: London · Email: p.miller@example.com` *(default language, only when different)*

---

Language Handling
-----------------

[](#language-handling)

The element stores the **default-language UID** (`sys_language_uid = 0`) of the selected record — consistent with how TYPO3 handles language overlays throughout the system.

Display is language-aware:

- The element reads the **backend user's preferred language** from `be_users.lang` (not the language of the record being edited)
- Both the selected card (server-rendered on page load) and the AJAX search results display the **translated title and all configured `infoFields`** in the editor's own language
- If no translation exists for a field, the default-language value is used as fallback
- When translated and default values differ, both are shown side by side (line 2 = translated, line 3 = default, italic)

### Cross-language search

[](#cross-language-search)

The AJAX search always runs two queries:

1. Default-language records matching the search term
2. Translation records (in any language) matching the search term → their default-language parent records are returned

This means editors can search in any language regardless of their backend language setting. A German editor searching for "Müller" will find the contact even when the backend is set to English.

---

Hidden Record Indicators
------------------------

[](#hidden-record-indicators)

The element shows a badge on the record title to indicate visibility problems:

BadgeColorMeaning`hidden`yellowThe default-language record **and** the editor's language translation are both hidden — or no translation exists and the default record is hidden`partially hidden`greyOne side is hidden: either the default-language record is hidden but the translation is visible, or the translation is hidden but the default record is visible*(none)*—All checked versions are visible### Scope of the check

[](#scope-of-the-check)

The check covers exactly **two records**:

1. The **default-language record** — the one whose UID is stored in the field
2. The **translation in the editor's display language** — resolved via `BackendUtility::getRecordLocalization()`

Hidden states of other language versions (e.g. a French translation when a German editor is working) are intentionally not checked. TYPO3's language model is hierarchical: the default-language record is the anchor, and editors are responsible for the language versions they work in. Checking all translations would require one extra DB query per result row, and surfacing a badge about a language the editor cannot even see in this context would be more confusing than helpful.

If a complete multi-language visibility check is required for a project, the `resolvePreviewImageUrl` approach — fetching all translation records in a single query — could be extended to also collect all hidden flags.

---

Security
--------

[](#security)

The element enforces TYPO3 backend permissions at two levels:

**Table-level:** The backend user must have `tables_select` permission for the foreign table. Requests for unknown or inaccessible tables are rejected with HTTP 400. The edit link on selected cards is hidden when the editor lacks `tables_modify` permission.

**Page-level:** Before running any record queries, the endpoint determines which pages (PIDs) the backend user may read. It first collects the distinct PIDs that contain matching records, then checks each one:

1. `isInWebMount()` — fast in-memory check against the user's configured web mounts
2. `readPageAccess()` — full page-permission DB check, only for pages that pass step 1

Records on inaccessible pages are excluded from all queries, not filtered after the fact. This ensures the result limit (`maxResults`) is not consumed by records the editor cannot see.

Root-level records (`pid=0`) are restricted to admin users by default. Set `allowRootLevel=true` in TCA to allow non-admin access.

Security-relevant settings (`allowRootLevel`, `allowRemoveInaccessible`) are baked into the server-generated HTML at render time and are never sent as client-controlled parameters.

---

How the AJAX Endpoint Works
---------------------------

[](#how-the-ajax-endpoint-works)

The element registers a backend AJAX route (`ajax_ot_recordselector_search`) that accepts the following query parameters:

ParameterDescription`table`Target table name`search`Search string (minimum 2 characters; multiple words are ANDed)`lang``sys_language_uid` of the record being edited (default: `0`)`backendLang``sys_language_uid` of the backend user's preferred language (default: `0`)`searchFields`Comma-separated columns to search in (optional)`infoFields`Comma-separated fields to include in the result info lines (optional)`maxResults`Maximum number of results (optional, default: 20, hard cap: 200)`returnUrl`URL to return to after editing a record (optional)`previewImageField`FAL field name for preview thumbnail (optional)The endpoint returns a JSON array:

```
[
  {
    "uid": 42,
    "title": "Peter Müller",
    "title_secondary": "Peter Miller",
    "hidden_status": null,
    "icon_identifier": "tt-address",
    "image_url": "/fileadmin/_processed_/portrait_64x64.jpg",
    "pid": 5,
    "page_path": "/contacts/",
    "edit_url": "/typo3/record/edit?...",
    "info_system": [
      { "label": "ID", "field": "uid", "value": "42" },
      { "label": "PID", "field": "pid", "value": "5" }
    ],
    "info_translated": [
      { "label": "Stadt", "field": "city", "value": "London" },
      { "label": "E-Mail", "field": "email", "value": "p.mueller@example.com" }
    ],
    "info_default": [
      { "label": "City", "field": "city", "value": "London" },
      { "label": "Email", "field": "email", "value": "p.miller@example.com" }
    ]
  }
]
```

- `title_secondary` contains the default-language title when it differs from the translated title (or `null`)
- `image_url` is `null` when no `previewImageField` is configured or no image is found
- `info_translated` is empty when the backend user's language is the default language
- `info_default` is empty when its values are identical to `info_translated`

Security: only columns listed in `$GLOBALS['TCA'][$table]['columns']` are accepted as search fields (whitelist approach). Access is checked against TYPO3 backend user permissions (`tables_select`).

---

License
-------

[](#license)

GPL-2.0-or-later — see [LICENSE](LICENSE)

Author
------

[](#author)

Oliver Thiele — [oliver-thiele.de](https://www.oliver-thiele.de)

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance89

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity37

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

3

Last Release

91d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/5030298?v=4)[Oliver Thiele](/maintainers/oliverthiele)[@oliverthiele](https://github.com/oliverthiele)

---

Top Contributors

[![oliverthiele](https://avatars.githubusercontent.com/u/5030298?v=4)](https://github.com/oliverthiele "oliverthiele (4 commits)")

###  Code Quality

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/oliverthiele-ot-recordselector/health.svg)

```
[![Health](https://phpackages.com/badges/oliverthiele-ot-recordselector/health.svg)](https://phpackages.com/packages/oliverthiele-ot-recordselector)
```

###  Alternatives

[friendsoftypo3/content-blocks

TYPO3 CMS Content Blocks - Content Types API | Define reusable components via YAML

103519.9k51](/packages/friendsoftypo3-content-blocks)[wazum/sluggi

TYPO3 extension for URL slug management with inline editing, auto-sync, locking, access control, and redirects

40529.5k](/packages/wazum-sluggi)[typo3/cms-scheduler

TYPO3 CMS Scheduler - Schedule tasks to run once or periodically at a specific time.

169.3M226](/packages/typo3-cms-scheduler)[typo3/cms-lowlevel

TYPO3 CMS Lowlevel - Technical analysis of the system. This includes raw database search, checking relations, counting pages and records etc.

178.2M307](/packages/typo3-cms-lowlevel)[typo3/cms-redirects

TYPO3 CMS Redirects - Create manual redirects, list existing redirects and automatically createredirects on slug changes.

167.4M77](/packages/typo3-cms-redirects)[typo3/cms-form

TYPO3 CMS Form - Flexible TYPO3 frontend form framework that comes with a backend editor interface.

147.6M257](/packages/typo3-cms-form)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
