Programmatic Registration and PHP Exports
Programmatic ACF registration gives you deterministic schema bootstrapping that does not depend on manual admin synchronization.
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.
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
| Approach | What Happens | Impact in Production |
|---|---|---|
Register groups on acf/init with stable keys | Schema boot order is deterministic on every request | Fewer outages caused by missing fields or ad-hoc admin sync |
| Mix manual admin edits and partial PHP exports | Two competing sources of truth emerge | Regression risk increases during deploy and rollback |
| Version PHP exports in Git with code review | Field changes are auditable and testable | Teams debug faster and avoid silent schema drift |
| Verify runtime groups and fields via WP-CLI | Deployment failures are detected before user traffic hits broken templates | Reduced incident MTTR and safer releases |
| Generate random keys per request (wrong pattern) | Existing meta mappings break because field keys no longer match stored references | Data appears missing; editors lose trust in admin and frontend output |
Reference Table
| Term/API | Signature/Syntax | Purpose | Key Notes |
|---|---|---|---|
acf/init | add_action('acf/init', callable $callback, int $priority = 10): void | Entry point for ACF-safe schema registration | Do not register groups before this hook |
acf_add_local_field_group() | `acf_add_local_field_group(array $field_group): array | false` | Register a full field group from PHP |
acf_add_local_field() | `acf_add_local_field(array $field): array | false` | Register one field dynamically |
acf_get_field_groups() | acf_get_field_groups(array $filter = []): array | Read runtime group registry for validation | Core CLI smoke-check primitive |
acf_get_fields() | `acf_get_fields(int | string | array $parent): array` |
update_field() | `update_field(string $selector, mixed $value, int | string $post_id = false): int | bool` |
have_rows() | `have_rows(string $selector, int | string $post_id = false): bool` | Iterate Repeater/Flexible rows |
get_row_layout() | `get_row_layout(): string | false` | Resolve active Flexible Content layout in loops |
wp eval-file | wp eval-file <path> | Execute full PHP scripts in WP context | Ideal for multisite or migration verification tasks |
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.
- Create a dedicated schema registration file in the plugin.
- Hook registration to
acf/init. - Define stable group and field keys with domain-prefixed names.
- Add a Pro Repeater field for structured highlights.
- Validate registration and field names with WP-CLI.
<?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',
],
],
],
]);
});
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"));'
groups=16
Array
(
[0] => Service Page Fields
)
Array
(
[0] => service_headline
[1] => service_intro
[2] => service_highlights
)
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.
- Add a registry class in the theme or plugin.
- Register the class callback on
acf/init. - Register multiple groups in one bootstrap pass.
- Include Pro Flexible Content for staff profile sections.
- Verify group registration and option values through WP-CLI.
<?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();
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"));'
Array
(
[0] => group_staff_directory
[1] => group_global_contact
)
+1-555-0100
Array
(
[0] => staff_position
[1] => staff_sections
)
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
<?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
<?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);
});
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;'
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'
If values suddenly disappear after a schema deploy, inspect key stability first. Name changes are usually recoverable; key churn is not.
Common Mistakes
| Mistake | Root Cause | What Breaks in Production | Correct Pattern |
|---|---|---|---|
Registering groups before acf/init | Callback attached to plugins_loaded or file-level execution | Missing groups on some requests; inconsistent behavior in CLI | add_action('acf/init', function (): void { acf_add_local_field_group([...]); }); |
| Using random or regenerated field keys | Developer treats keys as temporary IDs | Stored values no longer map to fields after deploy | Verify keys: wp eval 'print_r(array_column(acf_get_fields(acf_get_field_groups()[0]),"key"));' |
| Reusing the same field key across unrelated groups | Copy/paste without key policy | Conflicting definitions and undefined retrieval behavior | Check 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 registration | Deploy script order is wrong | update_field() writes fail or write to wrong structures | Run gate first: wp eval 'echo count(acf_get_field_groups()) . PHP_EOL;' |
| Missing location rules in exported arrays | Incomplete export editing by hand | Fields never appear in target post types/options pages | Assert locations: wp eval '$g=acf_get_field_groups(["key"=>"group_service_page"]); print_r($g[0]["location"]);' |
| No post-deploy CLI smoke check | Team relies on visual QA only | Broken schema reaches users before detection | Run 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
- Anchor all registration on
acf/init:add_action('acf/init', 'clinic_register_acf_groups'); - Treat keys as immutable IDs:
wp eval '$g=acf_get_field_groups(); print_r(array_column($g,"key"));' - Use domain-prefixed names for readability:
service_headline,staff_position,case_study_roi. - 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"));' - Run a runtime gate before data migrations:
wp eval 'if (!count(acf_get_field_groups())) { exit(1); } echo "ok" . PHP_EOL;' - Keep registration files in a known path:
wp-content/themes/clinic-pro/inc/acf/and review diffs together with templates. - 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
| Command | Purpose | Real Example Output |
|---|---|---|
wp eval 'echo count(acf_get_field_groups()) . PHP_EOL;' | Count runtime groups | 16 |
wp eval 'print_r(array_column(acf_get_field_groups(), "title"));' | List group titles | Array ( [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 group | service_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_headline | Read raw value for one post meta key | Programmatic 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.php | Run multisite schema health script | site=1 groups=16 |
| `wp plugin list --status=active | grep advanced-custom-fields-pro` | Confirm ACF Pro activation |
What's Next
- Continue to Performance, Debugging, Migration, and Launch Checklist.
- Return to Module 5 Overview to connect this lesson with Local JSON and deployment controls.
- Related lesson: Local JSON and Version Control Workflow.
- Related lesson: Understanding ACF Hook Lifecycle and Priorities.
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.