LUKDEV Custom Data Tables

Description

A WordPress plugin for defining custom data structures (sets of fields) and managing rows of data for them in the admin. The data is exposed through a public, read-only REST API, so a headless front end can use it.

Think of it as a small, standalone alternative to ACF options pages and repeaters. Each structure is a list of rows, stored in its own database tables instead of post meta.

Author: LUKDEV. Text domain: lukdev-custom-data-tables.

Requirements

  • WordPress 6.2+ (queries use the %i identifier placeholder of $wpdb->prepare())
  • PHP 7.4+

Usage

Go to LUKDEV Custom Data Tables in the admin menu. You need the manage_options capability.

  1. Create a structure. Give it a name and, optionally, a slug (the name is used when the slug is empty). Then add fields. A field’s key is generated from its label, for example Hero title becomes hero_title. Keys must be unique within a structure.
  2. Add rows. After you save the structure, the rows editor opens. Rows are saved one at a time, can be moved up or down, and can be deleted.
  3. Fetch the data from the REST API (see below).

Deleting a structure also deletes all its rows.

Field types

Each type is listed with its settings, the value stored in the database and the value returned by the API.

  • text: no settings. Stored: string. API: string.
  • textarea: no settings. Stored: string. API: string.
  • wysiwyg: no settings. Stored: HTML (wp_kses_post). API: string.
  • number: no settings. Stored: numeric string. API: string.
  • email: no settings. Stored: string. API: string.
  • url: no settings. Stored: string. API: string.
  • boolean: no settings. Stored: "1" / "0". API: true / false.
  • select: settings: options, default. Stored: option value. API: string.
  • radio: settings: options, default. Stored: option value. API: string.
  • checkbox: settings: options, defaults. Stored: list of option values. API: array of strings.
  • image: no settings. Stored: attachment ID. API: image object or null.
  • color_picker: settings: default, opacity, return format (string / array). Stored: #rrggbb or rgba(r,g,b,a). API: string, or { red, green, blue, alpha }.
  • date_picker: settings: display format, return format, first day of week. Stored: Ymd. API: date in the return format.
  • post_object: settings: post types, multiple. Stored: post ID or list of IDs. API: post object, null, or array of posts.
  • relationship: settings: post types, filters (search / post type / taxonomy), max. Stored: list of post IDs. API: array of posts.
  • link: no settings. Stored: { page, url, title, target }. API: { title, url, target } or null.
  • repeater: settings: sub-fields. Stored: list of items. API: array of objects.

Notes:

  • Options: an option without a value uses its label as the value, and the other way round.
  • Repeaters can’t be nested. A repeater added as a sub-field is saved as text.
  • Post object and relationship fields accept any public post type (except attachments) when no post types are selected. The API returns published posts only. Drafts and private posts picked in the admin are left out.
  • Link fields point to either a page or a custom URL. The page wins if both are set. The page’s title is used when the link text is empty.
  • Date picker fields use PHP date formats. The default for both display and return is d/m/Y.
  • Changing a field’s type or settings keeps saved data where possible. For example, a single post object becomes a list when multiple is turned on, and a select value becomes a one-item checkbox list. Fields saved before field types existed are treated as textarea.

REST API

Both endpoints are public (no authentication) and read-only.

GET /wp-json/ldcdt/v1/data

Lists all structures.

[
    {
        "name": "Team",
        "slug": "team",
        "fields": [
            { "key": "name", "label": "Name", "type": "text" },
            { "key": "photo", "label": "Photo", "type": "image" }
        ],
        "url": "https://example.com/wp-json/ldcdt/v1/data/team"
    }
]

GET /wp-json/ldcdt/v1/data/{slug}

Returns one structure with its rows, in the order set in the admin. Each item has an id plus one property per field key. Returns 404 (ldcdt_not_found) for an unknown slug.

{
  "name": "Team",
  "slug": "team",
  "fields": [ ... ],
  "items": [
    {
      "id": 1,
      "name": "Jane Doe",
      "photo": {
        "id": 42,
        "url": "https://example.com/app/uploads/jane.jpg",
        "alt": "",
        "width": 1200,
        "height": 800,
        "sizes": {
          "thumbnail": { "url": "...", "width": 150, "height": 150 }
        }
      }
    }
  ]
}

Shapes of the other object values:

  • Post (post_object, relationship): { id, type, slug, title, url, date }, where date is an ISO 8601 GMT date.
  • Link: { title, url, target }, where target is "" or "_blank".

Project structure

lukdev-custom-data-tables.php           Plugin header, constants, bootstrap
includes/
  install.php                  Table names, creation and upgrades
  fields.php                   Field types, sanitization and API formatting
  data.php                     Reading structures and rows
  rest-api.php                 REST routes (ldcdt/v1)
admin/
  admin.php                    Menu, assets, page router, notices
  handlers.php                 admin-post.php form handlers, post search AJAX
  partials/                    List, structure builder and rows editor screens
  css/, js/                    Admin assets
  vendor/select2/              Select2 4.1.0 (MIT)<h3>Extending</h3>

– To add a field type, register it in ldcdt_field_types(). Then handle its settings in ldcdt_sanitize_fields(), its value in ldcdt_sanitize_value() and ldcdt_format_value(), and its inputs in the admin partials and admin/js/admin.js.
– Everything uses the ldcdt prefix: functions (ldcdt_), constants (LDCDT_), tables, options, the REST namespace, CSS classes and JS. Sites upgraded from Custom Headless Data (old chd / chdata prefixes) have their tables and option renamed automatically. Admin form actions are ldcdt_save_structure, ldcdt_delete_structure, ldcdt_save_row, ldcdt_delete_row and ldcdt_move_row. The post search AJAX action is ldcdt_search_posts.

Screenshots

Installation

  1. Copy the lukdev-custom-data-tables directory to wp-content/plugins/ (web/app/plugins/ on Bedrock).
  2. Activate LUKDEV Custom Data Tables in Plugins.

On activation the plugin creates two tables. If the plugin is already active and the stored ldcdt_db_version option differs from LDCDT_DB_VERSION, it creates or updates them on plugins_loaded.

  • {prefix}ldcdt_structures: id, name, slug (unique), fields (JSON field definitions), created_at
  • {prefix}ldcdt_rows: id, structure_id, data (JSON values keyed by field key), sort_order

FAQ

Is the REST API protected?

No. Both endpoints are public and read-only, so don’t store private data in a structure. Editing data in the admin requires the manage_options capability.

Where is the data stored?

In two custom tables, {prefix}ldcdt_structures and {prefix}ldcdt_rows, not in post meta or options.

Can I nest repeaters?

No. A repeater added as a sub-field of another repeater is saved as a text field.

Why doesn’t a post I picked show up in the API?

The API returns published posts only. Drafts and private posts picked in a post object or relationship field are left out.

What happens to saved data when I change a field’s type?

The plugin keeps the data where possible. For example, a select value becomes a one-item checkbox list, and a single post object becomes a list when multiple is turned on.

Reviews

There are no reviews for this plugin.

Contributors & Developers

“LUKDEV Custom Data Tables” is open source software. The following people have contributed to this plugin.

Contributors

Changelog

0.1.0

  • First release.