How it works#

ACF Chef has two phases: describe and compile.

These phases explain why you can rearrange definitions, why some configuration runs later and why references can point to fields declared later.

Describe the field group#

During the describe phase, field declarations build a tree of lightweight ACF Chef nodes.

Each node records:

  • The field verb
  • Its name
  • Array configuration
  • Deferred configuration closures
  • Conditional rules
  • Any child fields or layouts

No extended-acf field object exists yet.

$group = FieldGroup::make('hero')
    ->trueFalse('has_image')
    ->group('content', fn ($content) => $content
        ->text('title')
        ->image('picture')->conditional('has_image', '==', true));

At this point, ACF Chef has described the group but has not constructed or serialised any extended-acf fields.

The description is still a tree, so you can clone it, address fields by path, modify it and add recipes to it. ACF Chef stores a deferred config() closure to run later.

Compile the field group#

Compilation starts when ACF Chef needs output, such as when you call toArray() or register the group.

ACF Chef compiles the group in this order:

  1. Normalise the group key into a stem, removing a leading group_ or field_.
  2. Save a snapshot of the current per-run state so a nested compilation cannot corrupt the outer one.
  3. Isolate extended-acf’s process-global key store.
  4. Build every node into a fresh extended-acf field object.
  5. Serialise each top-level field with the group stem.
  6. End key isolation and restore the previous key store.
  7. Rewrite every emitted key in the finished array.
  8. Rewrite references to those keys.

Both rewrite passes work recursively through sub_fields and flexible-content layouts.

The first pass replaces field and layout keys with the values produced by the active key strategy. The second updates settings that refer to those keys:

  • A repeater’s collapsed field
  • Every conditional rule’s field

ACF Chef rewrites references after building and serialising every field. This lets a conditional rule point to a field declared later in the group.

How each field is built#

Each node follows the same sequence:

  1. Resolve its verb to an extended-acf field class.
  2. Compute the compiled name and label.
  3. Construct the field with make($label, $name).
  4. Apply ordinary array settings through withSettings(), with ACF Chef’s eight reserved settings removed.
  5. Apply an explicit key supplied through configuration.
  6. Compute the path-based key and record the key mapping. Duplicate-key detection happens here.
  7. Build the node’s children and attach them through fields() or layouts().
  8. Apply the collapsed setting.
  9. Run deferred config() closures.
  10. Resolve and attach conditional rules.

You usually only need to consider this order when one kind of configuration depends on another.

Deferred configuration sees the complete field#

A deferred config() closure runs after ACF Chef applies array settings, children and collapsed.

$group->repeater('items', fn ($items) => $items
    ->text('title'))
    ->config(['collapsed' => 'title'])
    ->config(fn ($field) => $field->required());

The closure receives the assembled extended-acf field object. It can inspect or override earlier configuration, including array settings, attached children and the collapsed field.

The closure runs before ACF Chef attaches conditional rules.

Conditional rules behave the same in both APIs#

You can declare a condition on the cursor:

$group->text('caption')
    ->conditional('has_image', '==', true);

You can also declare it inside deferred configuration:

$group->text('caption')->config(fn ($field) => $field
    ->conditional('has_image', '==', true));

Both forms record the same kind of rule.

Deferred closures finish before ACF Chef resolves and attaches conditions, so conditions behave the same whichever form you use. Both forms use the same compilation steps for target lookup, boolean normalisation and final key rewriting.

Deliberate keys are not rewritten#

The compiler records its generated-key mapping before deferred closures run.

A key set later inside a deferred closure is therefore outside that generated-key mapping:

$group->text('title')->config(fn ($field) => $field
    ->key('field_legacy_title'));

The rewrite pass keeps that key unchanged.

The same rule applies to an explicit key supplied in array configuration:

$group->text('title')->config([
    'key' => 'field_legacy_title',
]);

Explicit keys remain exactly as supplied to preserve compatibility.

Compilation is repeatable#

extended-acf records generated keys in process-global state. Without isolation, compiling the same definition twice in one request can raise its duplicate-key error even when the group contains no collision.

ACF Chef saves a snapshot of that store, uses an empty store for the current compilation and restores the original in a finally block. Nested compilations have their own state and do not affect the surrounding compilation.

ACF Chef checks the keys it emits for duplicates. If two nodes produce the same final key, ACF Chef raises DuplicateFieldKey and identifies the node path that collided.

ACF Chef does not cache compiled field objects. extended-acf’s Field::toArray() changes the field’s settings, so each compilation builds fresh objects instead of reusing serialised ones.

$first = $group->toArray();
$second = $group->toArray();

assert($first === $second);

The definition remains unchanged, and repeated compilation produces the same array.