Macros and shorthands#

Macros and shorthands both extend ACF Chef, but they solve different problems.

Use a macro when you want new behaviour on a fluent field cursor.

Use a shorthand when one configuration key is really a convenient spelling for other configuration.

Cursor macros#

Register a macro with Chef::macro():

use AcfChef\Chef;

Chef::macro('half', function () {
    return $this->width(50);
});

The closure is rebound to the cursor before it runs.

Inside the macro, $this is the cursor you are configuring:

$group
    ->text('title')->half()
    ->text('summary')->half();

Re-registering the same macro name replaces the previous implementation.

Return $this to keep chaining#

A macro may return any value.

If you want the fluent chain to continue, return the cursor:

Chef::macro('requiredHalf', function () {
    $this
        ->config(['required' => true])
        ->width(50);

    return $this;
});

A macro that returns nothing returns null:

Chef::macro('half', function () {
    $this->width(50);
});

This call works:

$group->text('title')->half();

but there is no cursor left to continue from:

$group->text('title')->half()->text('summary');

Return $this when the macro is meant to participate in a fluent chain.

Macros run before cursor delegation#

A cursor checks registered macros before it delegates an unknown method to its parent container.

That means a macro can shadow a field verb:

Chef::macro('text', function ($value) {
    return $this->config([
        'instructions' => $value,
    ]);
});

On a cursor, this now runs the macro:

$group->image('picture')
    ->text('Shown beside the image');

It does not add a sibling text field.

Choose macro names carefully when they overlap registered verbs.

Real cursor methods always win#

Macros are only consulted through the cursor's dynamic method handling.

They cannot replace methods the cursor actually declares.

These methods never reach the macro table:

  • config()
  • conditional()
  • orWhere()
  • width()
  • add()
  • use()
  • find()
  • modify()
  • remove()

Registering a macro under one of those names does not override the real method.

Macros are cursor-only#

A macro is available on ACF Chef cursors.

It is not available directly on a field group or field set.

For example, this does not work:

Chef::macro('half', function () {
    return $this->width(50);
});

$group->half();

Neither does calling it on the FieldSet received by a nesting closure:

$group->group('content', function ($content) {
    $content->half();
});

The error can be misleading because those objects interpret an unknown fluent name as a potential field verb:

Unknown field type [half]. Register it with Chef::register(...).

Call the macro from a field cursor instead:

$group->group('content', fn ($content) => $content
    ->text('title')->half());

Macros extend cursor behaviour, not the whole builder surface.

Configuration shorthands#

Register a shorthand with Chef::shorthand():

Chef::shorthand('columns', function (int $count): array {
    return [
        'wrapper' => [
            'width' => intdiv(100, $count),
        ],
    ];
});

The shorthand receives the configured value and returns settings to merge:

$group->text('title')->config([
    'columns' => 4,
]);

This expands to:

[
    'wrapper' => [
        'width' => 25,
    ],
]

Re-registering an existing shorthand name replaces its previous implementation.

No shorthands ship by default#

The shorthand registry starts empty.

Every key in a config array reaches ACF as written, unless your application registered a shorthand for it:

$group->text('title')->config([
    'wrapper' => [
        'width' => 50,
        'class' => 'hero-title',
        'id' => 'primary-title',
    ],
]);

Folding width, class and id into wrapper automatically is a reasonable convenience, and one line away if you want it:

foreach (['width', 'class', 'id'] as $attribute) {
    Chef::shorthand($attribute, fn ($value): array => [
        'wrapper' => [$attribute => $value],
    ]);
}

Shipping that in the package would make a handful of config keys mean something other than the ACF setting of the same name, with nothing at the call site to say which. Which keys are special is a decision for the application writing them.

Shorthands expand only through config(array)#

Shorthand expansion happens in one place: a cursor's array form of config().

That determines where shorthands work.

Cursor config(array)#

This expands:

$group->text('title')->config([
    'columns' => 4,
]);

modify($path, array)#

This also expands because array-based modify() uses the same cursor configuration path:

$group->modify('title', [
    'columns' => 4,
]);

Deferred config(Closure)#

This does not use shorthand expansion:

$group->text('title')->config(function ($field) {
    // $field is the real extended-acf field object.
});

The closure is working against native extended-acf methods, not an ACF Chef config array.

FieldGroup::config()#

Group-level configuration does not expand field shorthands:

$group->config([
    'columns' => 4,
]);

That produces a literal columns key in the group settings.

width()#

The cursor's width() method does not consult the shorthand registry either:

$group->text('title')->width(50);

It writes wrapper.width directly.

A shorthand you register under width therefore does not change what width() does — the method never consults the registry.

Expansions merge with existing configuration#

A shorthand expansion participates in the normal config merge rules.

Associative arrays merge key by key. Lists are replaced wholesale.

An expansion also composes with an explicitly supplied wrapper array:

Chef::shorthand('columns', fn (int $count): array => [
    'wrapper' => [
        'width' => intdiv(100, $count),
    ],
]);

$group->text('title')->config([
    'wrapper' => [
        'data-role' => 'heading',
        'class' => 'hero-title',
    ],
    'columns' => 2,
]);

The resulting wrapper keeps all three values:

[
    'wrapper' => [
        'data-role' => 'heading',
        'class' => 'hero-title',
        'width' => 50,
    ],
]

Shorthands add configuration; they do not replace the surrounding structure unless the normal merge rules say that value should be replaced.

Macro or shorthand?#

Use a shorthand when the concept is fundamentally configuration:

Chef::shorthand('compact', fn (bool $enabled): array => [
    'wrapper' => [
        'class' => $enabled ? 'is-compact' : '',
    ],
]);

It then works anywhere a cursor config array is accepted, including dynamically built arrays and modify($path, array).

Use a macro when the concept is behaviour on a fluent chain:

Chef::macro('requiredHalf', function () {
    $this
        ->config(['required' => true])
        ->width(50);

    return $this;
});

If the result can be expressed entirely as settings, prefer a shorthand. It works in more places and remains usable when configuration is assembled as data.

Static-analysis support#

The generated IDE and PHPStan stubs cover registered field verbs.

They do not discover cursor macros you register yourself.

ACF Chef also ships a hand-written macros stub for the two macros it installs on extended-acf field objects so conditional() and orWhere() can be used inside deferred configuration closures.

That stub is separate from cursor macro registration.

A project-defined cursor macro such as:

Chef::macro('half', function () {
    return $this->width(50);
});

does not appear in the generated verb stub, so your editor and static analyser will not learn about it automatically.

See IDE and static analysis for what the generator does cover.