Skip to main content

Programmatic Registration and PHP Exports

Programmatic ACF registration gives you deterministic schema bootstrapping that does not depend on manual admin synchronization.

Learning Focus

You will implement a code-first schema registry using acf_add_local_field_group(), build a repeatable PHP export flow, and verify runtime correctness with WP-CLI before and after deployment. This is the practitioner-grade approach for teams that treat ACF as application infrastructure.

Concept Overview

When field groups are registered in PHP, schema state becomes part of your deployable codebase. That means feature branches can add or refactor fields safely, reviewers can audit schema diffs like any other code change, and CI can execute repeatable checks in WordPress runtime context.

PHP exports are especially useful when you need strict control over startup behavior. Instead of relying on one environment to "sync" field groups from admin, your application can register canonical groups on acf/init. This prevents environment drift and supports reproducible builds in containerized or immutable infrastructure setups.

In ACF Pro projects, this pattern becomes foundational because advanced field structures are often central to rendering: Repeater-driven sections, Flexible Content layouts, and Option-page defaults. A stable schema registry protects these structures from key collisions, missing layouts, and human error during release windows.

Core Idea

Use PHP-registered field groups as the source of truth for runtime schema, and use WP-CLI to prove the registry is loaded exactly as expected.

Variants you should understand:

  • Single-file export pattern: one file registers one group.
  • Registry class pattern: one bootstrap class registers multiple groups and shared field fragments.
  • Hybrid pattern: Local JSON for review plus PHP registration for deterministic runtime boot.
  • Multisite rollout pattern: same schema registry loaded on every site and verified by wp eval-file.

Why It Matters

ApproachWhat HappensImpact in Production
Register groups on acf/init with stable keysSchema boot order is deterministic on every requestFewer outages caused by missing fields or ad-hoc admin sync
Mix manual admin edits and partial PHP exportsTwo competing sources of truth emergeRegression risk increases during deploy and rollback
Version PHP exports in Git with code reviewField changes are auditable and testableTeams debug faster and avoid silent schema drift
Verify runtime groups and fields via WP-CLIDeployment failures are detected before user traffic hits broken templatesReduced incident MTTR and safer releases
Generate random keys per request (wrong pattern)Existing meta mappings break because field keys no longer match stored referencesData appears missing; editors lose trust in admin and frontend output

Reference Table

Term/APISignature/SyntaxPurposeKey Notes
acf/initadd_action('acf/init', callable $callback, int $priority = 10): voidEntry point for ACF-safe schema registrationDo not register groups before this hook
acf_add_local_field_group()`acf_add_local_field_group(array $field_group): arrayfalse`Register a full field group from PHP
acf_add_local_field()`acf_add_local_field(array $field): arrayfalse`Register one field dynamically
acf_get_field_groups()acf_get_field_groups(array $filter = []): arrayRead runtime group registry for validationCore CLI smoke-check primitive
acf_get_fields()`acf_get_fields(intstringarray $parent): array`
update_field()`update_field(string $selector, mixed $value, intstring $post_id = false): intbool`
have_rows()`have_rows(string $selector, intstring $post_id = false): bool`Iterate Repeater/Flexible rows
get_row_layout()`get_row_layout(): stringfalse`Resolve active Flexible Content layout in loops
wp eval-filewp eval-file <path>Execute full PHP scripts in WP contextIdeal for multisite or migration verification tasks
ACF Pro Required

Use cases in this lesson rely on Repeater and Flexible Content field definitions, which require ACF Pro.

Practical Use Cases

Use Case 1 — Register a service-page schema entirely in plugin code

A plugin team ships a reusable "Service Page" module to multiple client projects. They need schema registration to happen automatically after plugin activation, without any manual admin import.

  1. Create a dedicated schema registration file in the plugin.
  2. Hook registration to acf/init.
  3. Define stable group and field keys with domain-prefixed names.
  4. Add a Pro Repeater field for structured highlights.
  5. Validate registration and field names with WP-CLI.
wp-content/plugins/clinic-services/inc/acf/register-service-schema.php
<?php

declare(strict_types=1);

add_action('acf/init', function (): void {
acf_add_local_field_group([
'key' => 'group_service_page',
'title' => 'Service Page Fields',
'menu_order' => 0,
'position' => 'normal',
'style' => 'default',
'label_placement' => 'top',
'instruction_placement' => 'label',
'active' => true,
'fields' => [
[
'key' => 'field_service_headline',
'label' => 'Service Headline',
'name' => 'service_headline',
'type' => 'text',
'required' => 1,
'maxlength' => 120,
],
[
'key' => 'field_service_intro',
'label' => 'Service Intro',
'name' => 'service_intro',
'type' => 'textarea',
'rows' => 4,
'new_lines' => 'br',
],
[
'key' => 'field_service_highlights',
'label' => 'Service Highlights',
'name' => 'service_highlights',
'type' => 'repeater',
'layout' => 'table',
'button_label' => 'Add Highlight',
'sub_fields' => [
[
'key' => 'field_service_highlight_title',
'label' => 'Title',
'name' => 'service_highlight_title',
'type' => 'text',
'required' => 1,
],
[
'key' => 'field_service_highlight_body',
'label' => 'Body',
'name' => 'service_highlight_body',
'type' => 'textarea',
'required' => 1,
],
],
],
],
'location' => [
[
[
'param' => 'post_type',
'operator' => '==',
'value' => 'page',
],
],
],
]);
});
terminal: command
wp eval 'echo "groups=" . count(acf_get_field_groups()) . PHP_EOL;'
wp eval '$g=acf_get_field_groups(["key"=>"group_service_page"]); print_r(array_column($g,"title"));'
wp eval '$g=acf_get_field_groups(["key"=>"group_service_page"]); $f=acf_get_fields($g[0]); print_r(array_column($f,"name"));'
terminal: output
groups=16
Array
(
[0] => Service Page Fields
)
Array
(
[0] => service_headline
[1] => service_intro
[2] => service_highlights
)
warning

Never regenerate field keys after data has been saved. Field keys are metadata anchors, not cosmetic labels.

Use Case 2 — Use a registry class for multi-group exports and option defaults

A larger project needs two related schemas: one for staff profile posts and one for global contact defaults. The team uses a registry class so all groups register consistently in one place and can be validated by CLI.

  1. Add a registry class in the theme or plugin.
  2. Register the class callback on acf/init.
  3. Register multiple groups in one bootstrap pass.
  4. Include Pro Flexible Content for staff profile sections.
  5. Verify group registration and option values through WP-CLI.
wp-content/themes/clinic-pro/inc/acf/registry.php
<?php

declare(strict_types=1);

final class Clinic_Acf_Registry
{
public function boot(): void
{
add_action('acf/init', [$this, 'register_groups']);
}

public function register_groups(): void
{
acf_add_local_field_group($this->staffDirectoryGroup());
acf_add_local_field_group($this->globalContactGroup());
}

private function staffDirectoryGroup(): array
{
return [
'key' => 'group_staff_directory',
'title' => 'Staff Directory Fields',
'fields' => [
[
'key' => 'field_staff_position',
'label' => 'Staff Position',
'name' => 'staff_position',
'type' => 'text',
],
[
'key' => 'field_staff_sections',
'label' => 'Profile Sections',
'name' => 'staff_sections',
'type' => 'flexible_content',
'layouts' => [
[
'key' => 'layout_staff_bio',
'name' => 'staff_bio',
'label' => 'Bio',
'display' => 'block',
'sub_fields' => [
[
'key' => 'field_staff_bio_text',
'label' => 'Bio Text',
'name' => 'staff_bio_text',
'type' => 'textarea',
],
],
],
],
],
],
'location' => [
[[
'param' => 'post_type',
'operator' => '==',
'value' => 'staff_profile',
]],
],
];
}

private function globalContactGroup(): array
{
return [
'key' => 'group_global_contact',
'title' => 'Global Contact Settings',
'fields' => [
[
'key' => 'field_global_phone',
'label' => 'Global Phone',
'name' => 'global_phone',
'type' => 'text',
],
[
'key' => 'field_global_email',
'label' => 'Global Email',
'name' => 'global_email',
'type' => 'email',
],
],
'location' => [
[[
'param' => 'options_page',
'operator' => '==',
'value' => 'clinic-global-settings',
]],
],
];
}
}

(new Clinic_Acf_Registry())->boot();
terminal: command
wp eval '$groups=acf_get_field_groups(); print_r(array_column($groups,"key"));'
wp eval 'update_field("global_phone", "+1-555-0100", "option"); echo get_field("global_phone", "option") . PHP_EOL;'
wp eval '$g=acf_get_field_groups(["key"=>"group_staff_directory"]); $f=acf_get_fields($g[0]); print_r(array_column($f,"name"));'
terminal: output
Array
(
[0] => group_staff_directory
[1] => group_global_contact
)
+1-555-0100
Array
(
[0] => staff_position
[1] => staff_sections
)
note

This class pattern scales better than many disconnected registration files because key naming conventions and location logic stay centralized.

Use Case 3 — Edge case: unstable keys cause phantom data loss

A hurried developer generates keys with uniqid() during registration. Existing data remains in the database, but ACF cannot map old references to new keys, so values appear missing in admin and frontend.

❌ Fragile Pattern

wp-content/mu-plugins/acf-random-keys-fragile.php
<?php

declare(strict_types=1);

add_action('acf/init', function (): void {
acf_add_local_field_group([
'key' => 'group_' . uniqid(),
'title' => 'Fragile Service Group',
'fields' => [
[
'key' => 'field_' . uniqid(),
'label' => 'Service Headline',
'name' => 'service_headline',
'type' => 'text',
],
],
'location' => [[[
'param' => 'post_type',
'operator' => '==',
'value' => 'page',
]]],
]);
});

✅ Robust Pattern

wp-content/mu-plugins/acf-stable-keys-robust.php
<?php

declare(strict_types=1);

add_action('acf/init', function (): void {
$groupKey = 'group_service_runtime_stable';
$fieldKey = 'field_service_runtime_stable_headline';

acf_add_local_field_group([
'key' => $groupKey,
'title' => 'Service Runtime Stable Group',
'fields' => [
[
'key' => $fieldKey,
'label' => 'Service Headline',
'name' => 'service_headline',
'type' => 'text',
'required' => 1,
],
],
'location' => [[[
'param' => 'post_type',
'operator' => '==',
'value' => 'page',
]]],
]);

update_option('acf_stable_key_audit', [
'group_key' => $groupKey,
'field_key' => $fieldKey,
'registered_at' => gmdate('c'),
], false);
});
terminal: command
wp eval 'do_action("acf/init"); print_r(get_option("acf_stable_key_audit"));'
wp eval '$groups=acf_get_field_groups(); print_r(array_slice(array_column($groups,"key"), -3));'
wp eval 'echo var_export(get_field("service_headline", 1204), true) . PHP_EOL;'
terminal: output
Array
(
[group_key] => group_service_runtime_stable
[field_key] => field_service_runtime_stable_headline
[registered_at] => 2026-02-23T11:04:03+00:00
)
Array
(
[0] => group_service_page
[1] => group_staff_directory
[2] => group_service_runtime_stable
)
'Local JSON Verified'
warning

If values suddenly disappear after a schema deploy, inspect key stability first. Name changes are usually recoverable; key churn is not.

Common Mistakes

MistakeRoot CauseWhat Breaks in ProductionCorrect Pattern
Registering groups before acf/initCallback attached to plugins_loaded or file-level executionMissing groups on some requests; inconsistent behavior in CLIadd_action('acf/init', function (): void { acf_add_local_field_group([...]); });
Using random or regenerated field keysDeveloper treats keys as temporary IDsStored values no longer map to fields after deployVerify keys: wp eval 'print_r(array_column(acf_get_fields(acf_get_field_groups()[0]),"key"));'
Reusing the same field key across unrelated groupsCopy/paste without key policyConflicting definitions and undefined retrieval behaviorCheck duplicates: wp eval '$k=[]; foreach(acf_get_field_groups() as $g){foreach(acf_get_fields($g) as $f){$k[]=$f["key"];}} print_r(array_filter(array_count_values($k), fn($n)=>$n>1));'
Running migrations before schema registrationDeploy script order is wrongupdate_field() writes fail or write to wrong structuresRun gate first: wp eval 'echo count(acf_get_field_groups()) . PHP_EOL;'
Missing location rules in exported arraysIncomplete export editing by handFields never appear in target post types/options pagesAssert locations: wp eval '$g=acf_get_field_groups(["key"=>"group_service_page"]); print_r($g[0]["location"]);'
No post-deploy CLI smoke checkTeam relies on visual QA onlyBroken schema reaches users before detectionRun wp eval 'print_r(array_column(acf_get_field_groups(),"title"));' in release checklist
Deep Dive: Why Random Field Keys Are Harder to Debug Than They Look

Random keys can leave all your data physically intact while making it logically unreachable through ACF APIs. Teams often misdiagnose this as a migration failure or cache issue because the meta rows still exist in wp_postmeta. The real issue is key identity drift: ACF references field definitions by key, and value formatting rules also follow key mapping. You can waste hours debugging templates when the fault is actually schema identity instability. Catch it early by asserting key sets in runtime after every deploy.

wp eval '$g=acf_get_field_groups(); foreach($g as $group){echo $group["key"] . PHP_EOL; $fields=acf_get_fields($group); foreach($fields as $field){echo " - " . $field["key"] . " :: " . $field["name"] . PHP_EOL;}}'

Best Practices

  1. Anchor all registration on acf/init: add_action('acf/init', 'clinic_register_acf_groups');
  2. Treat keys as immutable IDs: wp eval '$g=acf_get_field_groups(); print_r(array_column($g,"key"));'
  3. Use domain-prefixed names for readability: service_headline, staff_position, case_study_roi.
  4. Verify field lists per group in CI: wp eval '$g=acf_get_field_groups(["key"=>"group_service_page"]); print_r(array_column(acf_get_fields($g[0]),"name"));'
  5. Run a runtime gate before data migrations: wp eval 'if (!count(acf_get_field_groups())) { exit(1); } echo "ok" . PHP_EOL;'
  6. Keep registration files in a known path: wp-content/themes/clinic-pro/inc/acf/ and review diffs together with templates.
  7. Validate option-context fields explicitly: wp eval 'echo get_field("global_phone", "option") . PHP_EOL;'

Hands-On Practice

Exercise 1: Register one deterministic group in code

Create wp-content/plugins/clinic-services/inc/acf/register-service-schema.php with Use Case 1 code and run:

wp eval '$g=acf_get_field_groups(["key"=>"group_service_page"]); echo count($g) . PHP_EOL;'

After completing this exercise, command output should be:

1

Exercise 2: Inspect registered field names and keys

Run the following checks:

wp eval '$g=acf_get_field_groups(["key"=>"group_service_page"]); $f=acf_get_fields($g[0]); print_r(array_column($f,"name"));'
wp eval '$g=acf_get_field_groups(["key"=>"group_service_page"]); $f=acf_get_fields($g[0]); print_r(array_column($f,"key"));'

After completing this exercise, output should include:

service_headline
service_intro
service_highlights
field_service_headline
field_service_intro
field_service_highlights

Exercise 3: Seed and verify a field value

Create a test page and seed one field value:

wp post create --post_title="ACF PHP Export Test" --post_status=publish --post_type=page
wp eval '$id=(int)get_page_by_title("ACF PHP Export Test", OBJECT, "page")->ID; update_field("service_headline","Programmatic Schema Live",$id); echo get_field("service_headline",$id) . PHP_EOL;'

After completing this exercise, final output should be:

Programmatic Schema Live

Exercise 4: Add registry class and verify options context

Add Use Case 2 registry class file and run:

wp eval 'update_field("global_phone", "+1-555-0200", "option"); echo get_field("global_phone", "option") . PHP_EOL;'

After completing this exercise, expected output:

+1-555-0200

Exercise 5: Detect unstable key pattern

Temporarily enable the fragile random-key example and run:

wp eval '$g=acf_get_field_groups(); print_r(array_slice(array_column($g,"key"), -5));'

After completing this exercise, if keys change between runs, you should observe:

group_65e....
group_65f....

Then restore the robust stable-key pattern and verify fixed output with:

wp eval 'print_r(get_option("acf_stable_key_audit"));'

CLI Reference

CommandPurposeReal Example Output
wp eval 'echo count(acf_get_field_groups()) . PHP_EOL;'Count runtime groups16
wp eval 'print_r(array_column(acf_get_field_groups(), "title"));'List group titlesArray ( [0] => Service Page Fields ... )
wp eval '$g=acf_get_field_groups(["key"=>"group_service_page"]); print_r(array_column(acf_get_fields($g[0]),"name"));'List fields in one groupservice_headline service_intro service_highlights
wp eval 'update_field("global_phone", "+1-555-0100", "option"); echo get_field("global_phone", "option") . PHP_EOL;'Write/read option field value+1-555-0100
wp post meta get 1204 service_headlineRead raw value for one post meta keyProgrammatic Schema Live
wp eval 'var_export(get_field("service_headline", 1204));'Validate formatted ACF retrieval'Programmatic Schema Live'
wp eval-file scripts/acf-multisite-verify.phpRun multisite schema health scriptsite=1 groups=16
`wp plugin list --status=activegrep advanced-custom-fields-pro`Confirm ACF Pro activation

What's Next

tip

Revisit this lesson when you are preparing a schema-heavy release and need to prove that field registration, key identity, and post-deploy data retrieval are stable.