Why ACF Chef#

ACF Chef is for field groups written in PHP that need to support 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.

This layer addresses problems with raw ACF arrays and plain extended-acf, but adds 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 must manage every structural detail:

  • Every group and field key must be created and kept stable by hand.
  • Nested sub_fields and layouts must 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 work with after declaration. To change a nested field, you must find and edit it in the finished array yourself.

Plain extended-acf#

extended-acf simplifies much of the syntax. It gives you field objects, fluent type-specific methods and a simpler way to produce the arrays ACF expects.

ACF Chef 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 has several behaviours that can affect larger definitions.

Generated keys are opaque#

extended-acf generates hashed field keys such as:

field_9c5d125a

Those keys work, but do not tell you which field they refer to when you see 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 refers 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.

This loses the supplied key.

ACF Chef compiles both the short and grouped conditional APIs through the same rule representation, which preserves explicit keys.

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.

Before producing the final ACF array, you can:

  • 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 produces extended-acf objects, then an 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.

The site therefore depends on field keys to read saved data correctly. Changing a key can make an existing value appear empty even though the old database row still exists.

ACF Chef 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 calls WordPress’s sanitize_title(). During registration, it calls acf_add_local_field_group(). Declaring and registering a group uses no other WordPress functions, actions or filters. Data migrations are a documented exception: they add acf/pre_load_metadata while a migration is pending and short-circuit acf/pre_update_metadata to determine what a write would do without performing it.

You can declare a field group without a database connection, the WordPress bootstrap or the ACF plugin. Tests that compile or register a group only need stubs for the small set of WordPress functions that operation calls.

use AcfChef\FieldGroup;

$group = FieldGroup::make('hero')
    ->text('title')
    ->location('post_type', 'page');

$this->assertSame(
    'field_hero_title',
    $group->toArray()['fields'][0]['key'],
);

This limited use of WordPress supports both assertions on the compiled ACF array and inspection of the uncompiled node tree.

See Testing for isolation and assertion options.

Costs and limitations#

ACF Chef adds another abstraction and another dependency, with the following limitations.

Custom field types require a class#

There is 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.

This lets the package handle registered types like built-in types, but requires a class even for a third-party type you use only once.

See Registering field types.

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 produces an undefined-method error rather than falling back to an untyped dynamic call.

See IDE and static analysis.

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 consuming code does not retain the ACF Chef key strategy. 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 must meet these version requirements to use the package.

When it is a good fit#

ACF Chef is a good fit when you manage field groups in code 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 starting 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’s additional layer is useful when you need to maintain field groups as application code.