PPPHP

Building Field Plugins

Field plugins are the little controls that decide how a single table field looks and behaves — a date picker for editing, a coloured badge for viewing, a range slider for filtering. This guide shows you how to build your own, from a blank file to a working control in the Designer, for beginners.

Who is this for? Owners and administrators. This guide is hidden from regular users. There is also an interactive companion at Plugin Manager → Developer Guide.

Contents

What is a field plugin? (four kinds)

When you design a table’s pages, each field can be assigned a control. There are four categories, each with one job:

CategoryFolderJobExample
Viewplugins/ViewShow a value on list/view pagesBadge, Currency, Image, Hyperlink
Editplugins/EditAn input on add/edit formsDate Picker, Dropdown, Checkbox, Password
Filterplugins/FilterA search control on list pagesText Filter, Date Range, Number Range
Validationplugins/ValidationA rule that checks input before savingEmail, Min length, Numeric range

Where plugins live & the file shape

Every plugin is a single PHP class in its own folder:

plugins/
  View/
    Badge/Plugin.php          ← a View plugin
    Currency/Plugin.php
  Edit/
    DatePicker/Plugin.php      ← an Edit plugin
    Dropdown/Plugin.php
  Filter/
    TextFilter/Plugin.php      ← a Filter plugin
  Validation/
    Email/Plugin.php           ← a Validation rule

The rules:

Anatomy of a plugin (the five methods)

Every View / Edit / Filter plugin implements the same five static methods:

MethodReturnsWhat it does
id()stringA unique id, e.g. 'Badge'.
label()stringThe friendly name shown in the picker.
render(…)string (HTML)Produces the control’s HTML. The signature differs per category (see below).
settingsSchema()arrayThe options shown in the properties panel.
assets()arrayExtra JS/CSS files: ['js'=>[], 'css'=>[]].

The one difference — the render() signature:

// View   — you get the value + settings, return display HTML
public static function render(mixed $value, array $settings): string

// Edit   — you also get the field NAME (for the <input name="…">)
public static function render(string $fieldName, mixed $value, array $settings): string

// Filter — the field name + settings (there is no single value to show)
public static function render(string $fieldName, array $settings): string

The settings panel (settingsSchema)

settingsSchema() returns a list of option definitions. Each becomes a control in the field’s properties panel, and the values come back to you in the $settings array. It uses the same field shapes as components:

public static function settingsSchema(): array
{
    return [
        ['key' => 'color',  'label' => 'Colour', 'type' => 'select',
         'options' => ['primary', 'success', 'warning', 'danger'], 'default' => 'primary'],
        ['key' => 'icon',   'label' => 'Icon',   'type' => 'text',   'default' => ''],
        ['key' => 'rounded','label' => 'Rounded','type' => 'bool',   'default' => 'no'],
    ];
}

Supported type values: text, number, select (with options), bool, color, textarea. Read the chosen values in render() from $settings['key'] — always with a fallback: $settings['color'] ?? 'primary'.

Example 1 — a View plugin (a coloured status badge)

Create plugins/View/StatusDot/Plugin.php. It shows a value with a small coloured dot — useful for status columns.

<?php
namespace PpPhp\Plugins;

use PpPhp\Core\ViewPlugin;

class StatusDot implements ViewPlugin
{
    public static function id(): string    { return 'StatusDot'; }
    public static function label(): string { return 'Status dot'; }

    public static function render(mixed $value, array $settings): string
    {
        $val = (string) ($value ?? '');
        if ($val === '') return '<span class="pp-muted">—</span>';

        // A colour per value, e.g. "active:success, pending:warning"
        $map = [];
        foreach (explode(',', (string) ($settings['colorMap'] ?? '')) as $pair) {
            if (!str_contains($pair, ':')) continue;
            [$k, $col] = array_map('trim', explode(':', $pair, 2));
            $map[strtolower($k)] = $col;
        }
        $color = $map[strtolower($val)] ?? (string) ($settings['color'] ?? 'secondary');

        // Always escape the value before printing it.
        return '<span class="pp-dot pp-bc-' . htmlspecialchars($color) . '"></span> '
             . htmlspecialchars($val);
    }

    public static function settingsSchema(): array
    {
        return [
            ['key' => 'color',    'label' => 'Default colour', 'type' => 'select',
             'options' => ['secondary', 'primary', 'success', 'warning', 'danger'], 'default' => 'secondary'],
            ['key' => 'colorMap', 'label' => 'Colour map (value:colour, …)', 'type' => 'text', 'default' => ''],
        ];
    }

    public static function assets(): array { return ['js' => [], 'css' => []]; }
}
Security: the value comes from the database and may contain anything. Wrap every value in htmlspecialchars() before you put it in HTML.

Example 2 — an Edit plugin (a native colour input)

Create plugins/Edit/ColorInput/Plugin.php. Edit plugins must render a form input whose name is the field name, so the value is submitted.

<?php
namespace PpPhp\Plugins;

use PpPhp\Core\EditPlugin;

class ColorInput implements EditPlugin
{
    public static function id(): string    { return 'ColorInput'; }
    public static function label(): string { return 'Colour picker'; }

    public static function render(string $fieldName, mixed $value, array $settings): string
    {
        $val  = (string) ($value ?? ($settings['default'] ?? '#4f46e5'));
        $name = htmlspecialchars($fieldName);
        $show = ($settings['showHex'] ?? 'yes') !== 'no';

        $html = '<span class="pp-colorpick">'
              . '<input type="color" name="' . $name . '" id="field_' . $name . '"'
              . ' value="' . htmlspecialchars($val) . '">';
        if ($show) {
            // A read-only text box that mirrors the picked colour (progressive JS).
            $html .= '<output>' . htmlspecialchars($val) . '</output>'
                   . '<script>(function(){var i=document.getElementById("field_' . $name . '");'
                   . 'if(!i||i.__b)return;i.__b=1;i.addEventListener("input",function(){'
                   . 'i.nextElementSibling.textContent=i.value;});})();</script>';
        }
        return $html . '</span>';
    }

    public static function settingsSchema(): array
    {
        return [
            ['key' => 'default', 'label' => 'Default colour', 'type' => 'color', 'default' => '#4f46e5'],
            ['key' => 'showHex', 'label' => 'Show hex value',  'type' => 'bool',  'default' => 'yes'],
        ];
    }

    public static function assets(): array { return ['js' => [], 'css' => []]; }
}

Key points for Edit plugins:

Example 3 — a Filter plugin

Create plugins/Filter/StartsWith/Plugin.php. Filter inputs must be named filter_<fieldName> so the list page picks them up.

<?php
namespace PpPhp\Plugins;

use PpPhp\Core\FilterPlugin;

class StartsWith implements FilterPlugin
{
    public static function id(): string    { return 'StartsWith'; }
    public static function label(): string { return 'Starts with'; }

    public static function render(string $fieldName, array $settings): string
    {
        $name = htmlspecialchars($fieldName);
        $ph   = htmlspecialchars((string) ($settings['placeholder'] ?? ('Starts with…')));
        return '<input type="text" name="filter_' . $name . '" class="form-control form-control-sm"'
             . ' placeholder="' . $ph . '">';
    }

    public static function settingsSchema(): array
    {
        return [
            ['key' => 'placeholder', 'label' => 'Placeholder', 'type' => 'text', 'default' => 'Starts with…'],
        ];
    }

    public static function assets(): array { return ['js' => [], 'css' => []]; }
}

Example 4 — a Validation rule

Validation plugins are slightly different: they check a value before it is saved. Create plugins/Validation/NoSpaces/Plugin.php.

<?php
namespace PpPhp\Plugins;

use PpPhp\Core\ValidationPlugin;

class NoSpaces implements ValidationPlugin
{
    public static function id(): string    { return 'no_spaces'; }
    public static function label(): string { return 'No spaces'; }
    public static function group(): string { return 'Format'; }

    public static function paramsSchema(): array { return []; }          // no options

    /** Field types this rule is offered for (hint only). */
    public static function appliesTo(): array { return ['text', 'email']; }

    /** {label} is replaced with the field's label. */
    public static function defaultMessage(): string { return '{label} must not contain spaces.'; }

    /** Return TRUE when the value is VALID. Empty values are skipped automatically. */
    public static function validate(mixed $value, array $params): bool
    {
        return !str_contains((string) $value, ' ');
    }
}

Validation rules appear in the field’s Validation section of the Designer. The same rule runs both in the browser and on the server, so bad data can never be saved.

Extra JS/CSS files (assets)

If your control needs a stylesheet or a script file, put them in the plugin folder and list them in assets(). They are copied into the built app and loaded only on pages that use the control.

public static function assets(): array
{
    return [
        'css' => ['style.css'],        // plugins/<Cat>/<Name>/style.css
        'js'  => ['behaviour.js'],     // plugins/<Cat>/<Name>/behaviour.js
    ];
}

For tiny scripts, a small inline <script> inside render() (as in the colour-input example) is fine and keeps everything in one file.

Bonus — option lists from a table (LookupOptions)

Dropdown-style controls often need their options from a database table. The LookupOptions helper does this for you: add its schema to yours, and resolve the options in render().

use PpPhp\Core\Designer\FieldControl\LookupOptions;

public static function render(string $fieldName, mixed $value, array $settings): string
{
    // Returns [ value => label, … ] from a manual list OR a table lookup,
    // based on the settings the user filled in.
    $options = LookupOptions::resolve($settings);
    // …build your <select>/buttons from $options…
}

public static function settingsSchema(): array
{
    return array_merge([
        // …your own options…
    ], LookupOptions::schema());   // adds "Options source", table, value/label columns, etc.
}

Installing & enabling your plugin

  1. Create the folder + Plugin.php under the right category (plugins/View, Edit, Filter or Validation).
  2. Open Plugin Manager in the console. Your plugin appears in its category; make sure it is enabled.
  3. In the Designer, select a field, open its properties, and choose your control under View control / Edit control / Filter. Its settings (from settingsSchema) appear right there.
  4. Build the project — the field now renders with your control on the published pages.
Core plugins are owner-managed. Your custom plugins live alongside them and are yours to edit. Use Plugin Manager → Developer Guide for the interactive version of this reference.

Checklist & safety notes

Done?Step
Folder plugins/<Category>/<Name>/Plugin.php created.
Class in namespace PpPhp\Plugins, implements the right interface.
All five methods present (id, label, render, settingsSchema, assets).
render() uses the correct signature for its category.
Edit control’s input uses name="<fieldName>"; Filter uses name="filter_<fieldName>".
Every value/setting printed to HTML is wrapped in htmlspecialchars().
Enabled in Plugin Manager and selected on a field in the Designer.
Golden safety rule: field values come from users and the database. Never put a raw value straight into HTML — always htmlspecialchars() it first. This prevents broken pages and cross-site-scripting (XSS) attacks.

See also: the Designer Components guide for building whole drag-and-drop blocks.