Back to overview

Creating a Custom Field Type

Developer Tutorials Published on 06.11.2025
Tutorial: Creating a Custom Field Type

Tutorial: Creating a Custom Field Type

Example: A "Star Rating" Field

Introduction: Understanding the Architecture

Before diving in, let's quickly recap the relevant parts of your application's architecture for custom fields:

  • custom_field_types (Database Table): Defines which field types are available in the system.
  • custom_fields (Database Table): Stores the definition of each field, including its field_type and a JSON options column.
  • admin_customfield_options.php: The page that renders the specific options for a field type.
  • field_types/ (Directory): Contains individual PHP files, each responsible for rendering a specific field type's HTML and its options.
  • field_renderer.php: A dispatcher that includes the appropriate rendering logic from the field_types/ directory for the record form.

Step 1: Define the Field Type

First, you need to make your new field type known to the system. This involves several small changes.

1.1 Database Entry

Add a new entry for RATING in your custom_field_types table with the following properties:

  • type_name: RATING
  • display_name_key: field_label_RATING
  • description_key: field_type_RATING_desc
  • field_group: special
1.2 Language Files

Add the display name and description for your new field type.

File: c:\Users\marti\Documents\xampp\htdocs\bt5test\languages\de.php

Add the following lines inside the `return [...]` array:

'field_label_RATING' => 'Bewertung (Sterne)',
'field_type_RATING_desc' => 'Ein interaktives Sternen-Bewertungsfeld.',

File: c:\Users\marti\Documents\xampp\htdocs\bt5test\languages\en.php

Create or open the file and add the following content:

<?php
// English Language File
return [
    // Field Types
    'field_label_RATING' => 'Rating (Stars)',
    'field_type_RATING_desc' => 'An interactive star rating field.',
    // Add other English translations as needed
];
?>
1.3 Documentation

Update your fieldtypes_help.md to describe the new field type.

File: c:\Users\marti\Documents\xampp\htdocs\bt5test\help\de\fieldtypes_help.md

Add the following section, for example before "Layout-Felder":

#### Bewertung (Rating)
Ein interaktives Feld zur Auswahl einer Sternen-Bewertung.
*   **Spezifische Optionen:** Maximale Anzahl Sterne, halbe Sterne erlauben.
*   **Allgemeine Optionen:** Feldbreite, Pflichtfeld, Sichtbar in Liste, Filterbar, Bedingte Sichtbarkeit.
*   **Praxisbeispiel:** Produktbewertung, Zufriedenheit mit einem Service, Schwierigkeitsgrad einer Aufgabe.

Step 2: Create the Field Type Class

To keep the code organized, we create a dedicated PHP class for our new field type. This class will handle rendering the specific options and processing the form submission for those options.

Create New File: c:\Users\marti\Documents\xampp\htdocs\bt5test\field_types\FieldTypeRating.php

<?php
class FieldTypeRating {
    private $field;
    private $errors;

    public function __construct(array $field, array &$errors) {
        $this->field = $field;
        $this->errors = &$errors;
    }

    public function renderInnerOptionsHtml() {
        $options = json_decode($this->field['options'] ?? '{}', true);
        $max_stars = $options['max_stars'] ?? 5;
        $allow_half_stars = !empty($options['allow_half_stars']);
        ?>
        <h4 class="h6 text-muted mb-3">Rating-Optionen</h4>
        <div class="row">
            <div class="col-md-6">
                <?php render_form_group([
                    'type' => 'number',
                    'name' => 'options[max_stars]',
                    'label_key' => 'Maximale Sterne',
                    'value' => $max_stars,
                    'min' => 1,
                    'max' => 20,
                    'required' => true
                ]); ?>
            </div>
            <div class="col-md-6 d-flex align-items-center pt-3">
                <?php render_form_switch([
                    'name' => 'options[allow_half_stars]',
                    'label_key' => 'Halbe Sterne erlauben',
                    'checked' => $allow_half_stars
                ]); ?>
            </div>
        </div>
        <?php
    }

    public function processPost(array &$field, array &$errors) {
        $options = json_decode($field['options'] ?? '{}', true);
        $options['max_stars'] = (int)($_POST['options']['max_stars'] ?? 5);
        $options['allow_half_stars'] = !empty($_POST['options']['allow_half_stars']);

        if ($options['max_stars'] < 1 || $options['max_stars'] > 20) {
            $errors['options[max_stars]'] = 'Die Anzahl der Sterne muss zwischen 1 und 20 liegen.';
        }
        
        $field['options'] = json_encode($options);
    }

    public function getFormattingOptions(): array {
        return []; // No special formatting options for this field type
    }

    public function getVisibilityOptions(): array {
        return ['is_required', 'is_visible', 'is_searchable', 'is_filterable'];
    }
}

Step 3: Create the Field Rendering File

This file contains the HTML, CSS, and JavaScript to render the star rating input in the actual record form.

File: c:\Users\marti\Documents\xampp\htdocs\bt5test\field_types\RATING.php

<?php
// This file is included by field_renderer.php and receives the following variables:
// $field: array - The field definition from custom_fields table.
// $record_data: array - The current record's data.
// $errors: array - Validation errors for the field.
// $is_readonly: bool - True if the form is in read-only mode.

$field_name = htmlspecialchars($field['field_name']);
$display_name = htmlspecialchars($field['display_name']);
$field_value = htmlspecialchars($record_data[$field_name] ?? '');
$error_message = $errors[$field_name] ?? '';
$is_required = !empty($field['is_required']);

// Decode options specific to the RATING field type
$options = json_decode($field['options'] ?? '{}', true);
$max_stars = (int)($options['max_stars'] ?? 5);
$allow_half_stars = !empty($options['allow_half_stars']);

$rating_id = 'rating-' . $field['id'];
?>

<div class="mb-3" data-field-type="RATING">
    <label for="<?= $field_name ?>" class="form-label">
        <?= $display_name ?>
        <?php if ($is_required): ?>
            <span class="text-danger">*</span>
        <?php endif; ?>
    </label>
    <div id="<?= $rating_id ?>" class="rating-input d-flex align-items-center" data-max-stars="<?= $max_stars ?>" data-allow-half-stars="<?= $allow_half_stars ? '1' : '0' ?>">
        <?php for ($i = 1; $i <= $max_stars; $i++): ?>
            <i class="fa-regular fa-star rating-star" data-value="<?= $i ?>"></i>
        <?php endfor; ?>
        <span class="rating-value-display ms-2 text-muted"></span>
    </div>
    <input type="hidden" name="<?= $field_name ?>" id="<?= $field_name ?>" value="<?= $field_value ?>" <?= $is_readonly ? 'disabled' : '' ?>>
    <?php if ($error_message): ?>
        <div class="invalid-feedback d-block"><?= $error_message ?></div>
    <?php endif; ?>
    <?php if (!empty($field['help_text'])): ?>
        <div class="form-text"><?= htmlspecialchars($field['help_text']) ?></div>
    <?php endif; ?>
</div>

<?php if (!$is_readonly): ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
    const ratingContainer = document.getElementById('<?= $rating_id ?>');
    if (!ratingContainer) return;

    const hiddenInput = document.getElementById('<?= $field_name ?>');
    const stars = ratingContainer.querySelectorAll('.rating-star');
    const valueDisplay = ratingContainer.querySelector('.rating-value-display');
    const maxStars = parseInt(ratingContainer.dataset.maxStars);
    const allowHalfStars = ratingContainer.dataset.allowHalfStars === '1';

    let currentValue = parseFloat(hiddenInput.value || 0);

    function updateStars(value) {
        stars.forEach(star => {
            const starValue = parseInt(star.dataset.value);
            star.classList.remove('fa-solid', 'fa-regular', 'fa-star', 'fa-star-half-stroke');
            if (value >= starValue) {
                star.classList.add('fa-solid', 'fa-star');
            } else if (allowHalfStars && value > starValue - 1 && value < starValue) {
                star.classList.add('fa-solid', 'fa-star-half-stroke');
            } else {
                star.classList.add('fa-regular', 'fa-star');
            }
        });
        valueDisplay.textContent = value > 0 ? `(${value.toFixed(allowHalfStars ? 1 : 0)} / ${maxStars})` : '';
    }

    ratingContainer.addEventListener('mousemove', function(e) {
        // ... (JavaScript logic remains the same)
    });
    // ... (rest of the JavaScript logic)
});
</script>
<style>
    .rating-input .rating-star {
        font-size: 1.5rem;
        color: var(--bs-warning);
        cursor: pointer;
    }
    .rating-input .rating-star.fa-regular {
        color: var(--bs-secondary-bg);
    }
</style>
<?php endif; ?>

Step 4: Implement Options UI in Admin

Now we connect our new FieldTypeRating.php class to the options page so that the specific settings can be configured.

Modify File: c:\Users\marti\Documents\xampp\htdocs\bt5test\admin_customfield_options.php

Add the following line at the top with the other require_once statements:

require_once __DIR__ . '/field_types/FieldTypeRating.php';

Then, find the switch statement and add the following case:

...
    case 'READ_ONLY':
        $fieldTypeHandler = new FieldTypeReadOnly($field, $errors, $all_table_fields);
        break;
    case 'RATING':
        $fieldTypeHandler = new FieldTypeRating($field, $errors);
        break;
    // Weitere Feldtypen-Handler werden hier später hinzugefügt
}
...

Step 5: Implement Rendering in Record Form

Now, you need to tell field_renderer.php to include your new RATING.php file when it encounters a field of type RATING.

Modify File: c:\Users\marti\Documents\xampp\htdocs\bt5test\field_renderer.php

...
        case 'CUSTOM_BUTTON':
            include 'field_types/CUSTOM_BUTTON.php';
            break;
        case 'RATING':
            include 'field_types/RATING.php';
            break;
        default:
            // Fallback for unknown types or simple text fields
            include 'field_types/TEXT.php';
...

Step 6: Handle Data Storage and Validation

For a simple numeric rating, the existing logic in custom_table_record_edit.php should handle it correctly, as the value is submitted via a hidden input field. No changes are needed for basic storage.

Ensure the database column for this field is of a suitable numeric type (e.g., DECIMAL(3,1) for up to 99 stars with half-steps, or TINYINT for whole numbers up to 255).

Step 7: Final Steps

  1. Clear Cache: If your application uses any caching for field definitions, clear it.
  2. Database Column: Ensure that the physical database table (data_team_{team_id}_{table_id}) has a column for your new RATING field. The system should automatically create this column when you add the field in the admin UI.
  3. Test:
    • Go to your table definition in the admin area.
    • Add a new field and select "Rating (Stars)" as the type.
    • Click the "Edit" icon for the new field to open the options page. You should now see the "Maximale Sterne" and "Halbe Sterne erlauben" settings.
    • Configure and save the options.
    • Test the new rating field in the record edit form.
By following these steps, you'll have successfully integrated a new, interactive custom field type into your CMS!