Conditional logic#
ACF represents conditional logic as groups of rules.
Rules inside a group are ANDed together. Groups are ORed against one another. A field is shown when every rule in at least one group matches.
ACF Chef gives you two ways to build that structure:
- A short fluent form for simple conditions.
- A closure form when the grouping needs to be explicit.
Both forms compile through the same path, can be mixed on one field and use the same target resolution, operator validation and value handling.
The short form#
Add a rule with conditional():
$group
->trueFalse('has_image')
->image('picture')
->conditional('has_image', '==', true);
Its signature is equivalent to:
conditional($target, $operator = '==', $value = null)
The operator defaults to ==, so this is valid:
$group->text('caption')
->conditional('status');
Repeated calls add rules to the current group:
$group->text('alt')
->conditional('has_image', '==', true)
->conditional('style', '==', 'wide');
That means:
has_image == true
AND
style == wide
Use orWhere() to start another group:
$group->text('note')
->conditional('has_image', '==', true)
->orWhere('caption', '!=empty');
That means:
has_image == true
OR
caption !=empty
Unlike conditional(), orWhere() requires an operator.
The closure form#
Pass a closure to conditional() when you want to write the grouping directly:
$group->text('overlay')->conditional(fn ($conditional) => $conditional
->group(fn ($rules) => $rules
->and('has_image', '==', true)
->and('style', '==', 'wide')
)
->group(fn ($rules) => $rules
->and('layout', '==', 'full')
)
);
The outer closure receives an AcfChef\Contracts\Conditional.
Each group() closure receives an AcfChef\Contracts\ConditionalRule.
Every group() creates an OR branch. Rules added with and() inside that group must all match.
The example means:
(has_image == true AND style == wide)
OR
(layout == full)
Use separate group() calls whenever the alternatives have more than one rule. The indentation then mirrors the shape ACF receives.
or() opens a new group#
Inside a group closure, or() starts another OR group:
$group->text('title')->conditional(fn ($conditional) => $conditional
->group(fn ($rules) => $rules
->and('style', '==', 'hero')
->or('style', '==', 'banner')
)
);
That creates:
(style == hero)
OR
(style == banner)
or() does not close the branch and return to the previous one.
Rules that follow it are added to the new group:
$group->text('title')->conditional(fn ($conditional) => $conditional
->group(fn ($rules) => $rules
->and('style', '==', 'hero')
->or('style', '==', 'banner')
->and('layout', '==', 'full')
)
);
This means:
(style == hero)
OR
(style == banner AND layout == full)
It does not mean:
(style == hero OR style == banner)
AND
layout == full
ACF has no syntax for factoring one rule across several alternatives. If layout == full must apply to both branches, write it in both:
$group->text('title')->conditional(fn ($conditional) => $conditional
->group(fn ($rules) => $rules
->and('style', '==', 'hero')
->and('layout', '==', 'full')
)
->group(fn ($rules) => $rules
->and('style', '==', 'banner')
->and('layout', '==', 'full')
)
);
Use or() when the alternative really is a short branch. Use separate group() calls when each branch has its own structure.
Empty groups are discarded#
Groups are opened lazily.
Calling group() does not immediately add an empty group to the condition. The group only exists once its first rule is added.
That makes conditional construction safe:
$group->text('title')->conditional(fn ($conditional) => $conditional
->group(function ($rules) use ($showAdvanced) {
if ($showAdvanced) {
$rules->and('layout', '==', 'full');
}
})
->group(fn ($rules) => $rules
->and('style', '==', 'wide')
)
);
If $showAdvanced is false, only the style group remains.
This matters because an empty conditional group would not mean “ignore this branch” to ACF. It would be a group with no rules to satisfy, which ACF treats as a branch that never matches.
The forms can be mixed#
Calling one form does not replace conditions declared through another.
ACF Chef appends conditional groups in a fixed order:
- Rules declared directly on the cursor with
conditional()andorWhere(). - Rules supplied through raw
conditional_logicconfiguration. - Rules recorded through
conditional()andorWhere()inside deferredconfig()closures.
Each group remains separate.
For example:
$group->text('title')
->conditional('style', '==', 'wide')
->config([
'conditional_logic' => [
[
[
'target' => 'layout',
'operator' => '==',
'value' => 'full',
],
],
],
])
->config(fn ($field) => $field
->orWhere('status', '==', 'featured'));
All three sources contribute to the final conditional logic rather than overwriting one another.
Operators#
ACF Chef accepts these operators:
><==!===pattern==contains==empty!=empty
Anything else raises InvalidArgumentException:
Invalid conditional logic operator [LIKE].
Operator validation happens at compile time, because that is when the stored rule is built.
Values#
Boolean values are normalised to the strings ACF expects:
true => '1'
false => '0'
null becomes an empty string:
null => ''
Everything else passes through unchanged.
That means these remain distinct:
0
'0'
''
The integer 0 stays an integer. The string '0' stays a string.
Whether a value was supplied matters#
ACF Chef records whether the value argument was present separately from the value itself.
These two calls are therefore different declarations:
$group->text('a')
->conditional('status', '==');
$group->text('b')
->conditional('status', '==', null);
For ==, both ultimately emit an empty-string value, but one omitted the argument and the other supplied it explicitly.
The distinction becomes visible with the valueless operators ==empty and !=empty.
Without a value:
$group->text('a')
->conditional('title', '!=empty');
the rule omits value:
[
'field' => 'field_hero_title',
'operator' => '!=empty',
]
If you explicitly supply one:
$group->text('b')
->conditional('title', '!=empty', 'x');
it is retained:
[
'field' => 'field_hero_title',
'operator' => '!=empty',
'value' => 'x',
]
and() and or() count their arguments the same way.
Target resolution#
Conditional targets are resolved against the complete field tree at compile time.
A target can therefore refer to a field declared before or after the field containing the rule.
There are two resolution modes: qualified paths and bare names.
Qualified paths start at the group root#
A target containing . or -> is a qualified path:
$group->group('content', fn ($content) => $content
->trueFalse('featured'));
$group->group('advanced', fn ($advanced) => $advanced
->text('note')
->conditional('content.featured', '==', true));
Qualified paths always start from the field-group root.
They are never interpreted relative to the current field.
The alternate separator means the same thing:
content.featured
content->featured
Bare names are resolved by proximity#
A bare target searches outward from the field containing the condition.
ACF Chef checks:
- The field’s own siblings.
- The parent’s siblings.
- The grandparent’s siblings.
- Each successive ancestor level out to the group root.
Within one sibling set, nearer fields win. At equal distance, the preceding sibling wins over the following one.
Because the whole tree already exists when resolution runs, declaration order does not limit the search. A following sibling is just as resolvable as a preceding one.
Bare-name lookup does not descend#
The proximity search checks sibling nodes. It does not recursively inspect the children of sibling containers.
Given:
$group->group('content', fn ($content) => $content
->trueFalse('featured'));
$group->group('advanced', fn ($advanced) => $advanced
->text('note'));
a condition on advanced.note cannot reach content.featured with:
->conditional('featured', '==', true)
featured is not a sibling visible from that position. It is a child of the sibling container content.
Use a qualified path instead:
->conditional('content.featured', '==', true)
Marker targets accept either name#
Conditional target matching understands both the declared and compiled spelling of marker fields.
For example:
$group->tab('Content');
$group->text('title')
->conditional('content', '==', 'something');
can resolve the tab even though the tab’s compiled field name is:
content_tab
Both the declared and compiled names are considered after sanitisation.
This differs from path lookup through find(), modify() and remove(), which follows the field’s declared path. See Finding and editing fields.
Unresolvable targets fail at compile time#
If no target can be found, ACF Chef raises UnresolvedReference instead of emitting a conditional rule pointing at a key that does not exist:
Conditional logic on [caption] refers to [nope], which is not in the field group.
Visible from there: [has_image].
The error occurs at compile time and applies to every ACF Chef conditional form:
conditional()orWhere()and()or()- Raw
conditional_logicarrays
The visible-field list reflects the names that were available to the bare-name resolver from that position.
Raw conditional_logic#
You can supply conditional rules through config():
$group->image('picture')->config([
'conditional_logic' => [
[
[
'target' => 'has_image',
'operator' => '==',
'value' => true,
],
],
],
]);
The outer value must be a list of groups, even when there is only one condition.
This is invalid:
$group->image('picture')->config([
'conditional_logic' => [
'target' => 'has_image',
'operator' => '==',
'value' => true,
],
]);
It raises:
Conditional logic on [picture] must be an array of rules.
Inside one group, a single rule may omit its own extra wrapping list:
$group->image('picture')->config([
'conditional_logic' => [
[
'target' => 'has_image',
'operator' => '==',
'value' => true,
],
],
]);
ACF Chef recognises that shape when the array contains a rule key such as target, name or operator.
Raw rule keys#
A raw rule accepts:
target, naming the field to resolve.name, as an alias fortarget.operator, defaulting to==.value, when the rule needs one.
A missing or non-string target raises:
Conditional logic on [picture] is missing a target field.
Rules supplied this way still use ACF Chef’s normal resolution, operator validation and value normalisation.
Passing through extended-acf conditional logic#
If conditional_logic contains an Extended\ACF\ConditionalLogic instance, ACF Chef passes that object through untouched.
It does not resolve its targets through the ACF Chef node tree and does not rewrite its keys.
Use that form when you deliberately want extended-acf’s own conditional API and its upstream behaviour.
Unsupported field types#
Not every extended-acf field type supports conditional logic.
In extended-acf 15, the two built-in exceptions are:
AccordionLayout
Trying to apply conditional logic to one raises BadMethodCallException naming the field path and verb rather than emitting a setting ACF cannot use.
Tab does support conditional logic.
See Configuring fields for how conditional rules declared inside deferred closures are collected, and Field keys for how their final field references are rewritten to the active key strategy.