Why ACF Chef#
ACF Chef is for field groups that are authored in PHP and expected to survive growth, reuse and refactoring.
It does not replace ACF or extended-acf. ACF still stores and renders the fields. extended-acf still supplies the field objects and their type-specific setting methods. ACF Chef adds a declaration tree around those objects, then compiles that tree into the array ACF expects.
That extra layer solves problems left by both raw ACF arrays and plain extended-acf. It also introduces costs of its own.
Raw ACF arrays#
acf_add_local_field_group() accepts the final field-group array directly. For a small, fixed definition, that may be all you need.
As the definition grows, you own every structural detail:
- Every group and field key must be created and kept stable by hand.
- Nested
sub_fieldsandlayoutsmust be assembled at the correct depth. - Conditional rules must point at exact field keys.
- Sharing a block of fields usually means copying part of an array or building your own abstraction around it.
A typo in a conditional field key is especially difficult to catch. ACF accepts the array, but the condition silently never matches because no field has that key.
There is also no field tree to address after declaration. Changing one nested field means navigating and mutating the finished array yourself.
Plain extended-acf#
extended-acf fixes much of the syntax. It gives you field objects, fluent type-specific methods and a cleaner way to produce the arrays ACF expects.
ACF Chef deliberately builds on that API rather than reproducing it. A deferred config() closure receives the real extended-acf field object, so its native setting methods remain available.
Plain extended-acf still leaves several behaviours that matter in larger definitions.
Generated keys are opaque#
extended-acf generates hashed field keys such as:
field_9c5d125a
Those keys work, but they do not identify the field when you encounter them in the database, a conditional rule or compiled output.
ACF Chef uses readable paths by default:
field_hero_content_title
Conditional references depend on compilation order#
extended-acf resolves a conditional target against fields it has already compiled.
A rule that points forward to a field declared later can therefore resolve to the wrong key. The definition still serialises, so the failure is easy to miss.
ACF Chef records the reference first and resolves it against the complete declaration tree during compilation. A target may appear before or after the field that refers to it.
Falsy conditional values can disappear#
extended-acf drops falsy conditional values while serialising a rule.
For example:
conditional('has_image', '==', false)
can become a rule with no value. ACF reads that as “equals an empty string”, which is not the same condition and can make the field appear at the wrong times.
ACF Chef preserves falsy values and normalises booleans to the strings ACF expects.
Chained AND rules can lose explicit keys#
extended-acf’s chained and() method stores an explicit field key under field, while its serialiser reads from key.
The supplied key is therefore lost.
ACF Chef compiles both the short and grouped conditional APIs through the same rule representation, so an explicit key survives.
A definition may only compile once#
extended-acf records generated keys in process-global state. Compiling the same group again can raise its duplicate-key error even though the definition itself contains no collision.
ACF Chef isolates that state for each compilation. The same definition can compile repeatedly in one request and produce the same array each time.
Each of these behaviours has a regression test in ACF Chef.
What the declaration tree adds#
Before compilation, an ACF Chef field group is a tree of lightweight PHP nodes rather than a collection of serialised field objects.
That makes several operations possible before the final ACF array exists:
- Resolve conditional targets against fields declared before or after the rule.
- Generate keys from a field’s complete path through the group.
- Find, modify or remove a nested field by path.
- Compose reusable recipes into any field container.
- Deep-clone shared definitions so one group’s changes do not affect another.
- Compile the same definition repeatedly for tests, inspection or registration.
The tree is temporary. Compilation still produces ordinary extended-acf objects and, ultimately, an ordinary ACF field-group array.
Readable keys are a compatibility feature#
ACF stores a field’s key alongside its value. The key connects saved data to the field definition that knows how to interpret it.
That makes a field key part of the site’s data contract. Changing it can make an existing value appear empty even though the old database row still exists.
ACF Chef therefore uses readable, path-based keys by default:
group_hero
field_hero_content
field_hero_content_title
They are predictable without compiling the group, searchable in the codebase and recognisable in the database.
Hashed extended-acf keys remain available for projects that already store them. Explicit legacy keys are also preserved exactly as supplied.
See Field keys for the available strategies and compatibility rules.
A small WordPress boundary#
Most of ACF Chef is ordinary PHP.
During key generation, it reaches WordPress through sanitize_title(). During registration, it calls acf_add_local_field_group(). That is the whole boundary: ACF Chef adds no actions or filters of its own.
A field-group definition does not require a database connection, the WordPress bootstrap or the ACF plugin merely to exist. Tests that compile or register one only need stubs for the small set of WordPress functions that operation reaches.
use AcfChef\FieldGroup;
$group = FieldGroup::make('hero')
->text('title')
->location('post_type', 'page');
$this->assertSame(
'field_hero_title',
$group->toArray()['fields'][0]['key'],
);
That boundary is useful whether you assert on the compiled ACF array or inspect the uncompiled node tree.
See Testing for isolation and assertion options.
Costs and limitations#
ACF Chef adds another abstraction and another dependency. The trade-offs are deliberate, but they are still trade-offs.
Custom field types require a class#
There is deliberately no generic builder method such as:
$fg->field('title', 'text', ['maxlength' => 60]);
A field type that extended-acf does not ship requires an extended-acf Field subclass and a Chef::register() call.
That keeps registered types first-class throughout the package, but it also means writing one class for a third-party type you may use only once.
Dynamic verbs need generated tooling#
Field verbs resolve through __call(). PHP can execute them, but an editor and PHPStan cannot infer the live registry automatically.
ACF Chef ships a stub generator, but running it is an additional project step. The stub must also be regenerated after the registry changes.
A stale PHPStan stub fails loudly with an undefined-method error rather than falling back to an untyped dynamic call.
fields() interop does not preserve compiled keys#
fields() returns extended-acf objects before they have been serialised. Code that consumes those objects calls their upstream toArray() method and receives extended-acf’s hashed keys.
The ACF Chef key strategy does not survive that handover. Conditional rules also fall back to upstream serialisation behaviour.
Use ACF Chef’s toArray() when field keys and resolved references matter.
See Locations and registration.
The version floor is high#
ACF Chef tracks extended-acf 15:
- PHP 8.4 or newer
vinkla/extended-acf^15.0- No extended-acf 14 support
A project that cannot meet that version floor cannot use the package.
When it is a good fit#
ACF Chef is a good fit when your field groups are code-managed and at least one of these is true:
- Existing values depend on stable, understandable field keys.
- Definitions contain nested structures or forward conditional references.
- The same block of fields appears in several groups.
- A shared definition needs small per-group changes.
- You want to inspect and test field groups without booting WordPress.
- You need project-specific field types or fluent helpers to behave like built-in ones.
Raw ACF arrays remain reasonable for a small definition that will not be shared or rearranged and whose keys you are comfortable maintaining yourself.
Plain extended-acf remains reasonable when its object API is enough, its generated keys match the site’s existing data, and its conditional and process-global key behaviour do not affect your definitions.
ACF Chef earns its extra layer when the field group itself needs to become maintainable application code.