TAW FrameworkData layer (schema)

Data layer — schema, JSON definitions and Boot::data()

Define post types, taxonomies, fieldsets and options pages in PHP or JSON, and use taw/core's data layer from both TAW themes — the classic taw-theme and the block theme taw-gutenberg.

One data layer, two TAW themes

taw/core is TAW's data layer. It defines your content model (post types, taxonomies, fields, options pages), stores values as post meta and options, exposes them over the REST API, and moves them between environments with Content Interchange. You don't need ACF, Carbon Fields or any other plugin.

Two themes consume it:

ThemeKindBoots
TAW ThemeClassic, with PHP blocks, Vite, Tailwind and AlpineTheme::bootstrapFullSite() — the data layer plus the classic-theme toolkit
TAW GutenbergBlock (full site editing) theme\TAW\Core\Boot::data() — the data layer only

taw/core is built for TAW sites only (taw-theme, taw-gutenberg and their child themes). It isn't a general-purpose plugin API for third-party themes.

Boot the data layer

A classic TAW theme already gets it: Theme::boot() calls Boot::data() itself. A data-only theme calls it directly, after requiring the Composer autoloader:

require_once __DIR__ . '/vendor/autoload.php';

\TAW\Core\Boot::data();

Boot::data() registers the Tools → TAW Data import/export screen, the content export REST route, REST-registered field meta, and the schema registry described below. It adds nothing to the front end: no Vite, no Alpine, and no performance tweaks, so a block theme keeps Gutenberg's block CSS. Calling it more than once is harmless.

Since taw/core v1.42.0, the Performance optimizations (which dequeue the block library CSS, among other things) register only when Theme::boot() or Theme::bootstrapFullSite() runs, not when the autoloader loads. If a setup relied on the old behavior without ever calling boot(), add define('TAW_PERFORMANCE_AUTOLOAD', true); to wp-config.php.

Define data in PHP

Hook into taw_schema_register. It fires on init, so translation functions are safe:

use TAW\Core\Schema\{Field, Registry, Schema};

add_action('taw_schema_register', function (Registry $schema): void {
    $schema->add(Schema::postType('book')->labels('Book', 'Books')->args(['menu_icon' => 'dashicons-book']));
    $schema->add(Schema::taxonomy('genre')->for('book')->labels('Genre', 'Genres'));
    $schema->add(
        Schema::fieldset('book_details')->title('Book details')->on('book')->fields([
            Field::text('book_author')->label('Author')->required(),
            Field::image('book_cover')->label('Cover'),
            Field::repeater('book_awards')->fields([Field::text('name')->label('Award')]),
        ])
    );
    $schema->add(Schema::optionsPage('library')->title('Library settings')->fields([Field::text('library_phone')]));
});

A fieldset compiles into a regular Metabox, and an options page into an OptionsPage. Storage, the admin UI, REST and content export work exactly like hand-written ones. Field::* builders produce the same arrays that new Metabox([...]) accepts, so you can mix builders and raw arrays, and ->with([...]) passes through any option the builder has no method for.

Define data in JSON

Put one entity per file in a taw-schema/ folder. Subfolders one level deep are fine (taw-schema/post-types/book.json). The keys use the same words as the PHP API:

{
  "$schema": "../vendor/taw/core/resources/schema/taw-schema-1.0.json",
  "version": 1,
  "kind": "fieldset",
  "key": "book_details",
  "title": "Book details",
  "on": ["book"],
  "fields": [
    { "id": "book_author", "type": "text", "label": "Author", "required": true },
    { "id": "book_awards", "type": "repeater", "fields": [{ "id": "name", "type": "text" }] }
  ]
}
kindKeys (besides version, kind, key and optional override)
post_typelabels (singular, plural), args — passed to register_post_type()
taxonomyfor (post types, required), labels, args — passed to register_taxonomy()
fieldseton (required), fields (required), title, context, priority, prefix, config
options_pagefields (required), title, menu_title, capability, config

The $schema line gives your editor autocomplete and inline errors. A typo in a top-level key is an error. Extra keys on a field, like conditions or width, pass through to the field engine.

Where files are found, and which one wins

Folders are scanned in this order: the child theme's taw-schema/, the parent theme's taw-schema/, then wp-content/taw-schema/ for site-level definitions that should survive a theme switch. When the same entity is defined twice, PHP beats every JSON file, a child theme beats its parent, and a theme beats wp-content. Set "override": true (or ->override() in PHP) to mark a replacement as deliberate. Otherwise you get a debug notice.

An invalid file is skipped with a notice, and the rest still load. In production, parsed files are cached until one of them changes.

Validate without WordPress

php bin/taw schema:validate            # this theme's taw-schema/
php bin/taw schema:validate path/ --json

It exits with an error on invalid files, pointing at each problem (for example /fields/2/type: must be one of text, url, …). It also warns about duplicates, and about fieldsets or taxonomies that target a post type these files don't define. Run it in CI.

Defaults and rules

Read the data

Values are post meta named _taw_<field id>:

use TAW\Core\Metabox\Metabox;

$author = Metabox::get($postId, 'book_author');
$awards = Metabox::get_repeater($postId, 'book_awards');   // decoded rows

Over REST, fields appear under meta (for example /wp-json/wp/v2/book/42), and structured fields also appear decoded as taw_<field id>.