taw/core PHP referenceMetabox and editor tooling

Metabox and editor tooling

Declare structured metaboxes in PHP, use the full field-type library, wire conditional logic, and retrieve saved values in your templates.

What metaboxes are in TAW

TAW Core's metabox engine (TAW\Core\Metabox\Metabox) lets you declare custom admin fields in pure PHP — no plugin required. It handles rendering in the WordPress editor, validation and sanitization on save, and retrieval helpers for templates.

You attach a metabox to a block by calling new Metabox([...]) inside registerMetaboxes() on any MetaBlock.

Minimal example

use TAW\Core\Metabox\Metabox;

new Metabox([
    'id'     => 'taw_hero',
    'title'  => 'Hero Section',
    'screens' => ['page'],
    'fields' => [
        ['id' => 'heading', 'label' => 'Heading', 'type' => 'text', 'required' => true],
        ['id' => 'image',   'label' => 'Image',   'type' => 'image'],
    ],
]);

Common field options

All field types share a base set of options:

OptionTypeDescription
idstringUnique field key (without prefix)
labelstringLabel shown above the field
typestringField type (see below)
descriptionstringHelp text shown below the field
placeholderstringInput placeholder text
defaultmixedDefault value
requiredboolMark field as required — validation runs on save
widthstringColumn width as a percentage, e.g. '50', '33.33'. Default '100'
conditionsarrayConditional logic — show/hide based on other field values
readonlyboolRender as non-interactive text; never written on save (see Readonly fields)
validatecallableYour own check: return true, or false / an error message
on_invalidstringWhen validation fails: 'store' (default) saves the input anyway and shows the error; 'keep' leaves the saved value as it was and shows what you typed again on the next load (taw/core v1.99.0+; a json field keeps by default)

Field types

Conditional fields

Fields can show or hide based on other field values. Conditions are evaluated live in the admin using Alpine.js, and also server-side on save. All conditions in the array use AND logic.

'fields' => [
    ['id' => 'show_cta', 'label' => 'Show CTA', 'type' => 'checkbox'],
    ['id' => 'cta_text', 'label' => 'CTA Text', 'type' => 'text',
     'conditions' => [
         ['field' => 'show_cta', 'operator' => '==', 'value' => '1'],
     ]],
    ['id' => 'cta_url', 'label' => 'CTA URL', 'type' => 'url',
     'conditions' => [
         ['field' => 'show_cta', 'operator' => '==', 'value' => '1'],
     ]],
],

Supported operators: ==, !=, contains, empty, !empty

Readonly fields

Set 'readonly' => true on any field whose value is authoritatively owned by something other than the wp-admin form — an external .md-file sync pipeline, a computed value — so the metabox stops implying it's editable there.

['id' => 'case_client', 'label' => 'Client', 'type' => 'text', 'readonly' => true],

A small lock icon appears next to the field's label (with a "Managed externally — read-only" tooltip), so the field reads as intentionally locked rather than as a bug in the admin UI.

This is enforced on both ends, not just visually. No <input>/<select>/<textarea> is rendered — there's nothing for devtools to re-enable and submit — and Metabox::save() skips the field entirely, so a forged $_POST key is ignored regardless of what the rendered markup looked like.

readonly composes with group and repeater rather than being its own field type:

  • On a group field, it propagates to every sub-field.
  • On a repeater field, it disables Add/Remove/reorder for the whole row list — not just the sub-field values — and propagates to every sub-field in every row. A synced list whose membership could still be edited would be just as misleading as an editable input. A readonly sub-field inside an otherwise-editable repeater is also supported and enforced per row.

Not currently enforced by TAW\Core\Rest\VisualEditorEndpoint or the fields:set/seo:inject CLI commands — those are separate write paths that don't consult this flag yet.

Tabbed metaboxes

Use the tabs key to group fields into tabs. Each tab references field IDs from the fields array.

new Metabox([
    'id'     => 'taw_hero',
    'title'  => 'Hero Section',
    'screens' => ['page'],
    'fields' => [
        ['id' => 'heading',  'label' => 'Heading',    'type' => 'text'],
        ['id' => 'image',    'label' => 'Image',      'type' => 'image'],
        ['id' => 'bg_color', 'label' => 'Background', 'type' => 'color'],
        ['id' => 'show_cta', 'label' => 'Show CTA',   'type' => 'checkbox'],
        ['id' => 'cta_text', 'label' => 'CTA Text',   'type' => 'text'],
    ],
    'tabs' => [
        ['id' => 'content', 'label' => 'Content', 'fields' => ['heading', 'image']],
        ['id' => 'design',  'label' => 'Design',  'fields' => ['bg_color']],
        ['id' => 'cta',     'label' => 'CTA',     'fields' => ['show_cta', 'cta_text']],
    ],
]);

Metabox config options

OptionDefaultDescription
screens['page']Post types, page slugs, or page template filenames to attach to — accepts a mixed array
context'normal'Position: 'normal', 'side', 'advanced'
priority'high'Order: 'high', 'default', 'low'
prefix'_taw_'Meta key prefix applied to all field IDs
icon(none)SVG string — displayed as the metabox icon
show_on(none)callable(WP_Post): bool — return false to hide the metabox
defaults(none)['field_id' => value] — what a field reads, and shows in the editor, while nothing is stored (taw/core v1.80.0+). See Defaults

Defaults

Put a block's fallback content in defaults instead of writing getMeta(...) ?: 'text' in getData():

new Metabox([
    'id'       => 'taw_hero',
    'screens'  => ['page'],
    'fields'   => [
        ['id' => 'hero_heading', 'type' => 'text'],
        ['id' => 'hero_items', 'type' => 'repeater', 'fields' => [['id' => 'title', 'type' => 'text']]],
    ],
    'defaults' => [
        'hero_heading' => __('Welcome', 'taw-theme'),
        'hero_items'   => [['title' => 'One'], ['title' => 'Two']], // structured fields as arrays
    ],
]);
  • Metabox::get(), getMeta(), get_repeater() and the other getters, and Taw::post()->field(), return the default while the stored value is empty as ?: saw it: nothing, '', '0', or an empty JSON list (a repeater saved with no rows stores '[]', an image field saved blank '0'). A checkbox's '0' is a value. The page renders as it did with ?: (taw/core v1.80.1+).
  • The edit screen shows the default in the field, so saving the post keeps it.
  • A default reaches only the posts its metabox applies to: its post types, templates and slugs (taw/core v1.95.2+). If each page variation has its own metabox with the same field ids, each page gets its own variation's defaults, and a variation that declares none shows none.
  • Options pages take the same key, and OptionsPage::get() falls back to it.
  • A ?: fallback lives only in code. A default can become a record: Tools → TAW Data → Defaults or taw content:defaults --apply writes the defaults into empty fields, with an undo. See Content Interchange.
  • The block editor's data panel doesn't show defaults yet.

Field availability in nav menu context

When metabox fields are rendered on the WordPress nav menu editor screen, only a subset of field types are available: text, url, number, textarea, select, and checkbox. Complex fields such as image, wysiwyg, color, files, and repeater are not supported in the nav menu context due to asset availability constraints.

Retrieval API

Since taw/core v1.57.0 there's also a typed API: $this->fields($postId)->field('hero_image')->image()->url() in a block, or Taw::post() anywhere, with values escaped when echoed. See Reading fields.

Use these static helpers inside getData() or anywhere in your templates:

use TAW\Core\Metabox\Metabox;

// Plain text / any scalar value
$heading = Metabox::get($postId, 'hero_heading');

// Checkbox → boolean
$showCta = Metabox::get_bool($postId, 'show_cta');

// Image attachment ID → URL
$imageUrl = Metabox::get_image_url($postId, 'hero_image', 'large');

// Color with fallback
$bgColor = Metabox::get_color($postId, 'bg_color', '#ffffff');

// post_select → array of post IDs
$featuredId = Metabox::get_posts($postId, 'featured_post')[0] ?? null;
$relatedIds = Metabox::get_posts($postId, 'related_posts');

// repeater → array of row arrays
$members = Metabox::get_repeater($postId, 'team_members');
foreach ($members as $member) {
    echo esc_html($member['name'] ?? '');
}

Inside a MetaBlock, the convenience wrappers delegate to the same methods:

protected function getData(int|false $postId): array
{
    return [
        'heading'   => $this->getMeta($postId, 'hero_heading'),
        'image_url' => $this->getImageUrl($postId, 'hero_image', 'large'),
    ];
}