Field groups registered in PHP
9 min read
A field group can live in code instead of in the editor. You call wpmcf_register_field_group() with a definition, and the plugin creates (or updates) the group and its database table for you. This suits themes and plugins that ship their own fields, and sites that keep everything in Git.
How it differs from the other ways to move definitions:
Registered in PHP | |||
|---|---|---|---|
Source of truth | The PHP code | The JSON files | One-off file |
Editable in the editor | No (read-only) | Yes | Yes |
Runs | Automatically when the code changes | When the files change | When you click Import |
Registered groups are real groups with real tables. The definitions are not read on every request: they are imported like a JSON file when the registered set changes. Values, queries, REST, shortcodes and blocks work exactly as for editor groups.
Quick start
<?php
// In your theme's functions.php or in a plugin.
add_action( 'wpmcf/field_groups/register', function () {
wpmcf_register_field_group( [
'key' => 'book_details',
'label' => 'Book details',
'fields' => [
[ 'key' => 'subtitle', 'label' => 'Subtitle', 'type' => 'text' ],
[ 'key' => 'pages', 'label' => 'Pages', 'type' => 'number' ],
],
'location' => [
[ [ 'param' => 'post_type', 'operator' => 'equal', 'value' => 'post' ] ],
],
] );
} );Open any page of WPM Fields in the admin. The group Book details appears in the list with a PHP badge, and the table {prefix}wpmcf_fg_book_details exists. Then read the values as usual:
echo esc_html( wpmcf_get_field( 'book_details', 'subtitle' ) );You can call wpmcf_register_field_group() from the wpmcf/field_groups/register action (the plugin fires it once, late on init) or earlier, for example directly in functions.php: the function only stores the definition. Calling it before this plugin has loaded is fine for the same reason.
The function returns true when the definition was accepted and false when it has no key or no fields list. Nothing else is checked at this point; everything else is validated when the group is imported (see Errors).
The definition format
A definition has the same shape as one entry of field_groups in an export file. The easiest way to start is to build a group in the editor, export it and translate the JSON to a PHP array.
wpmcf_register_field_group( [
'key' => 'book_details', // lowercase letters, digits, underscore; cleaned with sanitize_key()
'label' => 'Book details',
'description' => 'Shown under the editor.',
'fields' => [
[
'key' => 'subtitle',
'label' => 'Subtitle',
'type' => 'text',
'required' => true,
'instructions' => 'One short line.',
'placeholder' => 'e.g. A story',
'validation' => [ 'max_length' => 80 ],
],
[ 'key' => 'pages', 'label' => 'Pages', 'type' => 'number', 'default_value' => '100' ],
[
'key' => 'format',
'label' => 'Format',
'type' => 'select',
'settings' => [
'choices' => [
[ 'value' => 'hc', 'label' => 'Hardcover' ],
[ 'value' => 'pb', 'label' => 'Paperback' ],
],
],
],
[ 'key' => 'in_print', 'label' => 'In print', 'type' => 'true_false' ],
[
'key' => 'chapters',
'label' => 'Chapters',
'type' => 'repeater',
'sub_fields' => [
[ 'key' => 'name', 'label' => 'Name', 'type' => 'text' ],
[ 'key' => 'words', 'label' => 'Words', 'type' => 'number' ],
],
],
[
'key' => 'translated_title',
'label' => 'Title',
'type' => 'text',
'translation' => 'copy', // WPML / Polylang mode, Business plan only
],
],
// Outer list = OR, inner list = AND. 'operator' is 'equal' or 'not_equal'.
'location' => [
[
[ 'param' => 'post_type', 'operator' => 'equal', 'value' => 'post' ],
[ 'param' => 'post_status', 'operator' => 'equal', 'value' => 'publish' ],
],
[
[ 'param' => 'post_type', 'operator' => 'equal', 'value' => 'page' ],
],
],
'settings' => [
'style' => 'wpmcf', // wpmcf | standard | seamless
'position' => 'side', // high | normal | side
'label_placement' => 'left', // top | left
'instruction_placement' => 'label', // label | field
'order' => 5,
'active' => true,
'rest' => true, // Show in the REST API
'graphql' => true, // Show in GraphQL
'meta_bridge' => true, // Bridge get_post_meta() / update_post_meta()
'searchable' => true, // Include in site search
],
] );What is optional:
label,description,settings,location: omitted values get defaults (the settings defaults are those in Group settings). A group withoutlocationis created but is shown nowhere. A missinglabelgives an empty label, so always set one.- A field's
typedefaults totext; unknown types are an error. A field'ssettings,validation,conditional_logic,display,sql_column,default_value,indexedfollow the field options. created_atandupdated_atare ignored.- Keys must follow the editor's rules: a group key is lowercase letters, digits and underscores, starts with a letter and has at most 63 characters. A field key must start with a letter, use lowercase letters, digits and underscores (max 63) and must not be a reserved SQL word. Keys such as
sub-title,1storwhenmake the group fail to import. A group key with wrong characters is cleaned (Bad Key!becomesbadkey), so always write the clean form. - The same limits as the editor apply: an advanced field type that your plan does not include fails the import with the field type is not included in your plan, and Password fields cannot be sub-fields.
Field uids: renaming without losing data
Every field has a uid, the identity the plugin uses to recognise a field after it was renamed. If a field in your definition has no uid, one is derived from its position in the definition:
uid = "php-" + first 16 hex characters of md5( "{group_key}.{field_key}" )Sub-fields extend the path ({group_key}.{field_key}.{sub_key}). The derived uid is the same on every request, so a definition always maps to the same columns. The consequence is that changing a field's key without a uid creates a new, empty column: the old column is treated as the column of a removed field and keeps its data, it is not renamed (see Re-adding a removed field).
To rename a field and keep its values, give the field an explicit uid from the start (or at the moment of the rename, using the uid the field already has) and change only the key:
// Version 1
[ 'key' => 'subtitle', 'uid' => 'book-subtitle', 'label' => 'Subtitle', 'type' => 'text' ],
// Version 2: same uid, new key. The column is renamed and the values move with it.
[ 'key' => 'sub_title', 'uid' => 'book-subtitle', 'label' => 'Subtitle', 'type' => 'text' ],(Verified: a value saved under title2 was readable under the new key headline after the rename with the same uid.)
Find the uid of a field that you did not give one by exporting the group (WPM Fields › Tools) or by reading it with wp wpmcf export --items=field_groups:book_details. A uid is any non-empty string; use something readable that you will not reuse for another field.
Re-adding a removed field
Removing a field from the code never deletes data: its column stays as a leftover column. When you later register a field with that key again (for example after renaming a key away and back, or after an accidental removal), the sync takes the old values back automatically instead of failing. A code sync answers the editor's "what to do with the old data?" question with "reclaim it". (Verified: pages = 12, field removed, re-added: still 12; renamed away and back: still 12.)
Before you remove or rename fields in a group that already holds data, read Changing a field group safely.
When the group is created or updated
On every request the plugin runs, late on init (priority 99), these steps:
- It collects the definitions (the
wpmcf/field_groups/registeraction runs once). - It computes a hash of all definitions plus the plugin version.
- If the hash equals the one stored by the last sync, it stops. Nothing touches the database (a sync that reported errors is retried, see Errors).
- If it differs, it syncs now or schedules a background job:
Request | What happens |
|---|---|
wp-admin page (not AJAX) of a user who can | The sync runs immediately in that request. |
WP-CLI (any | The sync runs immediately. |
Visitor, front end, REST, cron, AJAX | One background job |
So after you deploy new code, the first admin who opens any wp-admin page (or the next Action Scheduler run) updates the tables. To do it deliberately, for example in a deployment script:
wp wpmcf code syncThe import is the same one the JSON import uses with Replace existing definitions on: the table is changed with the usual safety rules, data is never dropped, and the group is not mirrored into Local JSON files, because the code is its source.
Things that follow from this:
- Register the same set on every request. If you register groups only on the front end, or only in the admin, or depending on the user, the hash changes between requests and the plugin keeps syncing back and forth. Register unconditionally.
- Register on or before
wpmcf/field_groups/register. A definition added later in the same request (for example onwp_loaded) is not part of the hash for that request and will be missed or will flip the hash between requests. - Updating the plugin changes the hash (the version is part of it), so every registered group is imported once again after an update.
- Each sync replaces every registered group, so
wp wpmcf code syncreportsN updatedeven when nothing changed. That is normal. - After an admin or WP-CLI request already synced the changes, a following
wp wpmcf code syncshows0 created, N updated(the groups exist already).
In the editor
A group that comes from code is marked everywhere it appears:
- In the Field Groups list it has a PHP badge.
- Opening it shows an info notice Registered in PHP: This field group comes from code that calls wpmcf_register_field_group(). Change it there; edits made here cannot be saved.
- The Save changes button is disabled, so nothing you change in the form can be saved (the form itself is still shown). Through the REST API,
PUTandDELETEon the group return 409 with the codewpmcf_managed_by_code: This field group is registered in PHP code. Change it in the code that registers it. AGETshows"managed_by": "php". - Creating a group with the same key through the REST API fails (
422, key already in use).
The values of such a group are edited like those of any other group: on the edit screens, with wpmcf_update_field(), through REST and so on.
Other ways of changing the group are refused too. A JSON import (even with Replace existing definitions), Local JSON sync and the REST API cannot overwrite a group that code owns; the item fails with This field group is registered in PHP code. Change it in the code that registers it. (verified for a JSON import; Local JSON uses the same import path and was not tested separately). Only the code's own sync can change it.
Conflict with a group made in the editor
If a group with the same key already exists and was not created by code (you made it in the editor, or imported it), the code definition is not applied and the existing group is left exactly as it is. The sync reports:
book_details: a field group with this key was created in the editor; rename one of them.Rename the key in the code (or delete the editor group) and the next sync applies the definition.
Errors
A group that cannot be imported (an unknown field type, an unknown location parameter, a field key that is not allowed, a plan restriction, a column conflict) is skipped; the other groups still sync.
- A red notice appears on WPM Fields screens for users who can
manage_options: Some field groups registered in PHP could not be created or updated. They are retried on the next admin page load. followed by the messages, one per line, for examplebook_details: Field "x": the field type "nope" does not exist.orbook_details: Unknown location parameter "bogus". - The problem is also written to Settings › Health under the module
portability.code. - The failed sync is retried on every admin request until it succeeds (visitors never queue the same failing job again).
- The group that failed keeps its last good definition; its table is untouched.
Removing a group from the code
When you stop registering a group, the next sync releases it:
- The group, its table and all data stay.
- The PHP badge disappears and the group becomes editable like any editor group.
- Nothing is deleted. To delete it, use Delete field group in the editor.
The plugin remembers which groups it released. If you register the same key again later, the group is taken back: it is managed by code again (PHP badge, read-only) and the next sync applies the definition (verified). A group of the same key that was made in the editor and never came from code is still reported as a conflict (above).
WP-CLI
wp wpmcf code list
wp wpmcf code syncCommand | Output |
|---|---|
| A table with |
| Creates or updates all registered groups and their tables now. Prints each problem as a warning and |
Both commands are also described in WP-CLI commands. They work only where WP-CLI loads WordPress.
Hooks
Hook | Type | Use |
|---|---|---|
| action | Register groups here with |
| action (Action Scheduler job) | The background sync. Do not call it yourself; use |
| filter ( | Returns |
Limitations
- A definition is imported, not read at runtime. A change takes effect only after a sync (see above); a database change on a very large table runs in the background.
- Only field groups can be registered this way. Post types, taxonomies and options pages are managed in the plugin or via JSON import.
- Registered groups cannot be saved or deleted in the editor (the REST API answers 409), and imports cannot overwrite them.
- Removing a field from the code keeps its column and data (re-adding the key brings the data back).
- Each sync counts every registered group as
updated, even when nothing changed.
Still stuck? Contact support