Content Fields Runtime and API

This developer note documents the BricksMembers Content Fields runtime: field definition storage, postmeta and termmeta value ownership, Content Editor integration, Structures quick edit integration, Bricks Builder surfaces, REST/headless exposure, ACF migration utilities, AI/MCP abilities, and the invariants that keep content fields separate from profile fields and ACF runtime ownership.

Since version 1.3.0.

Scope

Content Fields are BRM-owned custom fields for WordPress content objects. They store definitions in one option and values as native post meta or term meta under the public field key. They are post/page/structure-item/taxonomy-term content fields, not user profile fields.

ACF is migration-only for this module. Do not register fake ACF groups, do not bridge runtime reads through ACF, and do not write ACF reference meta for BRM-owned fields.

Runtime Owners

  • ContentFieldsFeatureRegistrar owns feature-local bootstrap for post editor, term editor, and REST meta registration.
  • ContentFieldDefinitionService owns brm_content_field_groups, definition normalization, key validation, key uniqueness, key immutability, and MutationContract::settings_updated( 'content_fields' ).
  • ContentFieldValueService owns value sanitization, required-field validation, normal post value writes, term value writes, and subset writes used by Structures quick edit.
  • ContentFieldRuntimeBridge is the read adapter for dynamic tags, headless formatting, and other runtime consumers.
  • ContentFieldExposurePolicy is the single owner for public/headless exposure decisions based on settings.public.
  • ContentFieldsPostEditController owns the WordPress post editor meta box.
  • ContentFieldsTermEditController owns the WordPress taxonomy term add/edit screen integration and saves matching fields as native termmeta.
  • StructureContentEditorDataProvider and StructureContentEditorAjax adapt matching Content Fields into the Structure Content Editor.
  • StructureQuickEditAjax adapts opted-in Content Fields into the hierarchy Quick Edit modal and saves only the opted-in subset shown in that modal.
  • ContentFieldRenameMigrationService owns key rename migrations: dry-run, copy old-key post meta to new-key post meta, then delete the old key.
  • src/Modules/ContentFields/Migration/ owns ACF analysis, definition mapping, value migration, and migration manifests.

Bootstrap

  1. BootstrapCoordinator::boot_modules() boots ContentFieldsFeatureRegistrar::boot() when ModuleRegistry::is_active( 'content_fields' ) is true.
  2. The registrar calls ContentFieldsPostEditController::register() and ContentFieldsTermEditController::register().
  3. The registrar adds ContentFieldRestMetaRegistrar::register() to init.
  4. UtilityBootstrap wires Content Fields dynamic tag registration and rendering behind the same module gate.
  5. BricksModuleHookRegistrar owns Bricks query-loop, query-filter, and form-action hook registration for Content Fields.
  6. BricksConditionHookRegistrar owns Bricks condition hook registration for Content Fields and delegates through BricksConditionsRegistrar.

No Content Fields provider class should call add_filter() or add_action() for Bricks directly when an existing registrar owns that hook family.

Definition Contract

The definition option is brm_content_field_groups. It stores an array of groups with location rules and fields. A field definition has an immutable key, a type, labels/descriptions, required state, UI hints, settings, and optional subfields.

Field keys are global across all groups and subfields. The key is the live content meta key, so changing it without migration would orphan values. ContentFieldDefinitionService rejects duplicate keys, private keys beginning with _, reserved WordPress/Bricks/BRM/ACF-like keys, and saved-key changes for an existing field ID.

Canonical settings include:

{
  "public": false,
  "quick_edit": false,
  "bricks": {
    "filter_element": false,
    "query_loop_filter": false,
    "condition": false
  },
  "choices": {},
  "default_value": "",
  "return_format": "id",
  "min": "",
  "max": "",
  "step": ""
}

settings.public is the only public/headless-readable flag. REST, headless output, and type catalog checks must all read that single flag through ContentFieldExposurePolicy.

Value Storage and Writes

Scalar post values are saved as native WordPress post meta under the field key, and taxonomy-term values are saved as native term meta. This is intentional: disabling the module leaves authored scalar values in place, and Bricks native custom-field dynamic data can still read post meta keys when the user knows them.

  • ContentFieldValueService::save_values_for_post() validates required fields before writing any submitted values.
  • sanitize_value() branches by field type. Media and relationship values normalize to IDs; multiple-value fields normalize to arrays; group and repeater values recursively sanitize subfields.
  • Normal admin, Structure Content Editor, Bricks Form, and AI/MCP value writes must delegate to ContentFieldValueService.
  • Rename and import services may write copied postmeta as explicit migration utilities with manifests.

Admin and Editor Surfaces

  • ContentFieldsPage renders the content-fields admin surface.
  • assets/admin/js/content-fields-page.js owns the modal editor controller for the definition UI. The main admin page renders compact field-group cards; group and field edits happen in modal dialogs, and the hidden save form posts normalized definitions to ContentFieldsAdminActionHandler.
  • ContentFieldsAdminActionHandler handles definition saves, ACF import preparation, and rename migration requests.
  • ContentFieldsPostEditController renders matching fields in a WordPress post editor meta box, enqueues the Content Fields value UI, and saves through save_values_for_post().
  • ContentFieldsTermEditController renders matching fields on taxonomy term add/edit screens. Taxonomy-targeted fields save through save_values_for_term() and use native termmeta under the public field key.
  • StructureContentEditorDataProvider::get_content_fields_payload() exposes matching fields, values, and renderer HTML to the Structure Content Editor so scalar, link, media, relationship, group, and repeater controls share the same value UI as the post editor.
  • StructureContentEditorAjax::save_content_fields() writes submitted Content Fields for the selected post after nonce, request-method, edit_posts, and edit_post checks.
  • StructureQuickEditAjax::get_quick_edit_fields() returns a rendered Content Fields fragment for non-structured fields where settings.quick_edit=true. quick_edit_post_fields() saves that subset with save_values_for_post_fields(), so required fields outside Quick Edit do not block unrelated quick edits.

The Structure Content Editor has two separate custom-field surfaces: ACF-compatible field HTML and BRM-owned Content Fields. Keep that distinction intact.

Bricks Builder Surfaces

Bricks hook registration is intentionally split by hook family:

  • BricksModuleHookRegistrar registers Content Fields query types, structured loop controls, actual Query Loop controls, Filter element source hooks, and the brm_update_content_fields form action.
  • BricksConditionHookRegistrar registers Content Fields condition options/result hooks by pairing registrar methods on BricksConditionsRegistrar.
  • ContentFieldsBricksIntegration is provider wiring for query-loop object types only.
  • ContentFieldFilterElementIntegration is the provider for the brmContentField Bricks Filter element source.
  • ContentFieldQueryLoopFilterIntegration injects opt-in Content Field controls into actual Bricks post and term Query Loop elements.
  • ContentFieldConditionIntegration is the provider for opt-in Content Fields conditions.

ContentFieldsFormIntegration resolves the target post from a mapped Post ID field, the static Post ID setting, or the current post. It authorizes writes with normal edit_post access, then falls back to InstructorAccessService::user_can_edit_item_content() when the Instructor Frontend Builder module is active. It still delegates all value writes to ContentFieldValueService.

Dynamic tags are always available while the module is active. Bricks Filter element exposure, actual Query Loop controls, condition exposure, and Structures quick-edit exposure are not automatic. The definition layer normalizes unsupported opt-ins back to false, including settings.quick_edit on group and repeater fields.

  • {brm_cf:field_key} reads a direct stored field value.
  • {brm_cf:field_key:label} maps select, button group, radio, and checkbox stored values to their configured choice labels.
  • {brm_cf:group.subfield} reads a group subfield directly.
  • {brm_cf_loop:subfield_key} reads a subfield inside a Content Fields group/repeater loop object.
  • brmContentFieldGroupQuery, brmContentFieldRepeaterQuery, brmContentFieldGalleryQuery, brmContentFieldRelationshipQuery, brmContentFieldTaxonomyQuery, and brmContentFieldUserQuery expose structured, media, relationship, taxonomy, and user loop objects.
  • brmContentField Filter element source lists only fields with settings.bricks.filter_element=true and a supported Filter element type.
  • brmCfQl_{field_key}_mode, brmCfQl_{field_key}_value, brmCfQl_{field_key}_value2, and brmCfQl_{field_key}_match are generated only for supported post or term fields with settings.bricks.query_loop_filter=true.
  • brm_content_field__{field_key} conditions list only fields with settings.bricks.condition=true and a supported condition type.

Do not put \Bricks\* classes in method signatures. Use mixed parameters and runtime guards such as defined( 'BRICKS_VERSION' ), class_exists( '\Bricks\Query' ), and is_callable() where needed so PHPStan and unit tests continue to run without Bricks.

REST and Headless Exposure

ContentFieldRestMetaRegistrar registers supported public Content Fields as native post meta REST fields. Every register_post_meta() call sets an explicit auth_callback that requires edit_post for writes.

Remember that show_in_rest makes values readable to users who can read the post. The write auth_callback does not make the value private. For external/headless exposure, use settings.public deliberately and leave it false for non-public fields.

HeadlessRestContentFormatter adds content_fields to formatted content records only when the Content Fields module is active. It returns values from ContentFieldRuntimeBridge::get_public_values_for_post(), so headless output follows the same settings.public policy.

ACF Migration

ACF migration utilities live under src/Modules/ContentFields/Migration/.

  • ContentFieldAcfImportAnalyzer reads ACF groups and fields when acf_get_field_groups() and acf_get_fields() are available.
  • ContentFieldAcfDefinitionMapper maps supported ACF field definitions to native Content Fields definitions and reserves destination keys through ContentFieldDefinitionService.
  • ContentFieldAcfValueMigrator branches reads by ACF field type. Scalar, choice, relational, and media fields read raw values. Groups and repeaters use have_rows(), the_row(), and raw get_sub_field( $name, false ) where possible, with formatted fallback for structured values.
  • ContentFieldMigrationManifestService persists the latest migration manifest in brm_content_fields_last_import_manifest.

Nested relational and media subfields must normalize to IDs. Never write ACF reference meta for BRM-owned fields.

AI/MCP Abilities

AI Abilities exposes Content Fields through the content ability service when AI Abilities and the relevant access gates are enabled.

  • bricksmembers/content-fields-catalog-get returns module availability, the definition option, field groups, type definitions, and the Bricks prefix.
  • bricksmembers/content-fields-values-get reads values for a post and optional field list.
  • bricksmembers/content-fields-values-set-plan prepares a guarded value update plan through AiOperationPlanner.
  • bricksmembers/content-fields-acf-import-plan prepares ACF import analysis and points callers to the migration flow for spike-gated value migration.

The admin surface blueprint for Content Fields must list the exact Bricks resources in AiBricksCatalogData: form action brm_update_content_fields, query types, filter source brmContentField, condition pattern brm_content_field__{field_key}, and dynamic tag family content_fields.

Tests and Drift Guards

  • ContentFieldDefinitionServiceTest covers duplicate keys, key immutability, reserved/private keys, and Bricks exposure setting normalization.
  • ContentFieldValueServiceTest covers sanitization, required-field behavior, termmeta writes, and subset saves for Structures quick edit.
  • StructureQuickEditContentFieldsTest covers the Structure hierarchy Quick Edit AJAX path: only opted-in fields save, non-opted fields are ignored, and required opted-in fields block the save.
  • ContentFieldsTermEditControllerTest covers term edit rendering and term field saves.
  • ContentFieldRenameMigrationServiceTest covers dry-run and copy/delete key migration.
  • ContentFieldAcfValueMigratorTest covers type-branched ACF value reads and structured field handling.
  • ContentFieldBricksExposureTest covers opt-in Filter element, Query Loop control, and condition exposure, stale disabled-field rejection, and condition evaluation.
  • ContentFieldMetaQueryBuilderTest and ContentFieldQueryLoopFilterIntegrationTest cover typed Content Field meta-query clauses, serialized array matching, post query vars, term query vars, and archive query argument preservation.
  • ContentFieldsFormIntegrationTest covers mapped Post ID targeting and the Instructor Frontend Builder edit-permission fallback for frontend Content Fields saves.
  • DynamicTagsTest covers Content Fields dynamic tag registration and group subfield rendering.
  • AiAbilitiesFeatureTest and composer test:ai-mcp-drift defend the AI/MCP surface, blueprint/catalog parity, ability count, and docs contracts.

Implementation Invariants

  • Definitions have one option owner: ContentFieldDefinitionService.
  • Values have one normal write owner: ContentFieldValueService.
  • Field keys are globally unique and immutable after first save.
  • Rename means migration, never silent key mutation.
  • Dynamic tags and structured query loops are always available while the module is active.
  • Bricks Filter element exposure, actual Query Loop control exposure, and Bricks condition exposure are separate opt-ins per supported field.
  • Public/headless exposure reads only settings.public.
  • ACF is migration-only. Do not add an ACF bridge mode.
  • Do not add Bricks hook registrations outside the established registrars.
Get BricksMembers

Start Building Your Membership Site Today

Create, sell, and manage your content without limits. BricksMembers gives you everything you need to build membership and LMS sites with Bricks Builder.

Lifetime updates & bug fixes • Premium support • 0% transaction fees • 60-day money-back guarantee