Nesting and chaining#
Container fields nest through closures. Fluent chains continue through cursors.
The closure decides where children belong. The cursor decides what a call after a field declaration applies to.
Nest fields with a closure#
The four built-in container verbs are:
grouprepeaterflexibleContentlayout
Pass a closure as the second argument:
$group->flexibleContent('sections', fn ($sections) => $sections
->layout('media', fn ($media) => $media
->image('picture'))
->layout('quote', fn ($quote) => $quote
->textarea('words')));
The resulting field paths are:
field_hero_sections
field_hero_sections_media
field_hero_sections_media_picture
field_hero_sections_quote
field_hero_sections_quote_words
There is no end() or endGroup(). The closure boundary is the nesting boundary.
The closure receives a field container#
A nesting closure receives an AcfChef\Contracts\FieldContainer.
Concretely, it is a FieldSet, not a cursor.
That means config() is not available on the closure parameter itself:
$group->group('content', function ($content) {
// $content is a FieldSet.
});
Configure each field by chaining from the method that added it:
$group->group('content', fn ($content) => $content
->text('title')->config(['required' => true])
->textarea('summary')->config(['rows' => 4]));
To configure the container itself, continue after its closure:
$group->group('content', fn ($content) => $content
->text('title'))
->config(['width' => 50]);
A container verb always returns its own cursor, never the cursor returned by the last child inside the closure.
Add-methods return cursors#
Every add-method returns a cursor pointing at the node it just created:
$title = $group->text('title');
A cursor has its own methods for working with that field, including config(), conditional(), orWhere() and width().
For a method the cursor does not declare, Cursor::__call() follows a fixed resolution order.
How cursor calls are resolved#
An unknown cursor method is resolved in this order:
- A registered macro.
- Delegation to the parent container, when delegation is enabled.
- A registered field verb added inside the current field, but only when delegation is disabled.
- An exception.
The order is deliberate.
On an ordinary chaining cursor, a verb must continue to mean “add a sibling”:
$group
->group('content', fn ($content) => $content
->text('title'))
->text('next');
next is a sibling of content.
If the cursor tried the verb against the field it points at before delegating, next would silently become a child of content instead.
Delegation keeps a chain moving#
Consider:
$group
->text('title')
->textarea('summary');
text('title') returns the cursor for title.
That cursor does not declare textarea(), so it delegates the call to its parent field set. The field set recognises textarea as a registered verb and adds summary beside title.
The returned cursor now points at summary, so the chain can continue.
Calls can bubble to the field group#
Delegation is not limited to field verbs.
This works:
$group
->text('title')
->trueFalse('featured')
->location('post_type', 'page');
The cursor for featured does not declare location(), so it passes the call to its field set.
The field set does not declare location() either, so it escalates the call to the field group.
The field group does declare location(), so the location rule is added there. The call returns the field group.
Delegation only succeeds because an ancestor genuinely owns the method. It does not make arbitrary unknown methods valid.
Macros run before delegation#
Registered cursor macros have first refusal on an unknown method.
If a macro exists under a name, it runs before the cursor considers its parent container:
Chef::macro('half', function () {
return $this->width(50);
});
$group
->text('title')->half()
->text('summary')->half();
A macro name therefore wins over anything the same call might otherwise reach through delegation.
See Macros and shorthands for defining them.
Found cursors do not delegate#
Cursors returned by find() and supplied to modify() have delegation disabled.
That changes how a registered verb is interpreted.
On an ordinary chaining cursor, the verb delegates outward and adds a sibling.
On a non-delegating cursor, the verb instead means “add inside the field I point at”:
$content = $group->find('content');
$content->text('kicker');
If content is a container, kicker is added inside it.
This keeps targeted edits local. A call on a cursor obtained for one path cannot escape through delegation and unexpectedly add a sibling elsewhere.
See Finding and editing fields for the path APIs.
Unknown methods fail differently#
The cursor's delegation mode is visible in its errors.
On a normal delegating cursor, an unknown method eventually reaches verb lookup. If no verb is registered, ACF Chef raises UnknownFieldType.
For a close match within edit distance two, the exception suggests the likely verb:
$group->tex('title');
The error can suggest text.
On a non-delegating cursor, an unknown method raises BadMethodCallException instead. Its message says that delegation is off, making it clear why the call did not become a sibling field.
That difference is intentional: targeted cursors fail locally instead of allowing a typo to change another part of the definition.
width() is a real cursor method#
width() is implemented directly on the cursor:
$group->text('title')->width(50);
It merges wrapper.width.
It composes with a wrapper supplied through field configuration:
$group->text('title')
->config([
'wrapper' => [
'class' => 'hero-title',
'id' => 'primary-title',
],
])
->width(50);
The resulting wrapper contains all three settings:
[
'wrapper' => [
'class' => 'hero-title',
'id' => 'primary-title',
'width' => 50,
],
]
width() does not replace the existing wrapper.
It is the only wrapper convenience in the package: nothing in a config() array folds into wrapper unless your application registered a shorthand that does.
See Configuring fields for field configuration and merge behaviour.