Using ACF Builder with Sage


Advanced Custom Fields has quickly become a staple in WordPress theme development. It is incredibly powerful for handling custom field data and is jampacked with just about every field type your heart could desire. If it doesn’t have a specific field type you need for your project, it is extremely likely that it is somewhere out in the wild in the form of an ACF Extension as well as being incredibly easy to create yourself using a field boilerplate and the ACF documentation.

Despite the above, one thing it leaves to be desired is a maintainable, sane method to register fields programmatically with PHP. While it does allow us to add fields with PHP using acf_add_local_field_group(), it can quickly become extremely cumbersome to do so even for those of us who feel vastly more comfortable being inside of a text editor in comparison to using ACF’s out of the box field editor.

There are various reasons you may want to write and maintain your ACF fields with PHP instead of the default field editor such as:

  • Version controlling your fields without cryptic JSON exports
  • Reducing database queries
  • Programmatically altering field creation/values, such as having a repeater in theme options with a label -> value that then creates a set of fields available for your post type
  • The comfort of staying inside of your text editor while building your site

Registering a field group with PHP

Let’s take a look at ACF’s native approach for adding a simple field group for our posts with a single text and textarea field using PHP:

    'key' => 'group_1',
    'title' => 'My Group',
    'fields' => [
            'key' => 'field_title',
            'label' => 'Title',
            'name' => 'title',
            'type' => 'text',
            'key' => 'field_description',
            'label' => 'Description',
            'name' => 'description',
            'type' => 'textarea',
    'location' => [
                'param' => 'post_type',
                'operator' => '==',
                'value' => 'post',
    'menu_order' => 0,
    'position' => 'normal',
    'style' => 'default',
    'label_placement' => 'top',
    'instruction_placement' => 'label',

Even after changing ACF’s default documentation example to shorthand array’s and stripping away optional configuration, it is easy to imagine this turning into an unmaintainable nightmare if we were to begin adding the complexity most of us need in our field groups, let alone adding grouped field types such as repeater fields and flexible content fields.

Introducing ACF Builder

ACF Builder is a library written by StoutLogic that provides a fluent, chainable interface for building the arguments necessary to pass field groups along with their fields to acf_add_local_field_group(). While not only providing a much more readable, approachable syntax, it also provides a lot of sane defaults and transformations behind the scenes that allow us to focus more on building our fields without having to continuously define convoluted defaults. With ACF Builder, the need for defining field keys, label placement, or even a title becomes entirely optional.

Let’s take a look at adding our above field group but with ACF Builder’s syntax instead, but this time, adding a conditional to toggle our title and description fields as well as adding instructions while maintaining fewer lines than the default approach shown above:


use StoutLogic\AcfBuilder\FieldsBuilder;

$post = new FieldsBuilder('post');

    ->setLocation('post_type', '==', 'post');

    ->addTrueFalse('enable_example', ['ui' => 1])
        ->setInstructions('Enables our example fields shown below.')

        ->setInstructions('This is your title field.')
        ->conditional('enable_example', '==', '1')

        ->setInstructions('This is your description field.')
        ->conditional('enable_example', '==', '1');


Even with my unnecessary line breaking for readability purposes, we still come in at 12 fewer lines with the addition of a TrueFalse field, instructions, and our conditional. But, this still leaves the issue of long-term maintainability. Multiply the above with 30 fields along with repeaters and multiple field locations and you are suddenly scrolling through a 300+ line PHP file. But! With some clever PHP and ACF Builder’s ->addFields() method, we can create a way to make this much more manageable in our projects.

Installing ACF Builder

With ACF Builder being a Composer package, installation is extremely straightforward while using Sage and/or Bedrock.

$ composer require stoutlogic/acf-builder

Setting up ACF Builder

Once installed, create a folder called fields inside of our app folder. This is where we will separate our fields into partials and components, and our field groups/locations into appropriately named files corresponding to the post type the fields are attached to.

Before we get started, we need to open app/setup.php and import ACF Builder’s FieldsBuilder namespace like so:

namespace App;

use Roots\Sage\Container;
use Roots\Sage\Assets\JsonManifest;
use Roots\Sage\Template\Blade;
use Roots\Sage\Template\BladeProvider;
use StoutLogic\AcfBuilder\FieldsBuilder;

After importing the FieldsBuilder namespace, append the following at the bottom of your app/setup.php:

 * Initialize ACF Builder
add_action('init', function () {
    collect(glob(config('theme.dir').'/app/fields/*.php'))->map(function ($field) {
        return require_once($field);
    })->map(function ($field) {
        if ($field instanceof FieldsBuilder) {

The above is using glob() to collect an array of every PHP file in our fields folder and then passing the returned value to acf_add_local_field_group() if it is an instance of FieldsBuilder. Doing this will allow us to return our FieldsBuilder instance within each of our field files and in turn, automatically autoload and initialize them with acf_add_local_field_group().

While we are at it, let’s go into app/helpers.php and append a helper function to help include our fields in our field groups. This simply lets us use . instead of / for climbing directories as well as handles our include and field path for us to help tidy things up a bit.

 * Simple function to pretty up our field partial includes.
 * @param  mixed $partial
 * @return mixed
function get_field_partial($partial)
    $partial = str_replace('.', '/', $partial);
    return include(config('theme.dir')."/app/fields/{$partial}.php");

Creating our Partials

Now that we have our autoloader and helper in place, let’s create our first real field group with ACF Builder, but this time, let’s do it in a more reusable, DRY approach.

We will start by creating a folder called partials inside of our fields folder.

For this example, we will assume that we are organizing all of our field groups with Tabs and most, if not all field groups will have a custom tab of some kind, but will also need our General and Header tab which will allow for some basic per-page configuration for our theme such as setting an intro and subtitle, and perhaps toggling if we should automatically display the featured image and social sharing buttons on this post/page/CPT.

General Tab

For our General tab, we will create partials/general.php and it might look something like this:


namespace App;

use StoutLogic\AcfBuilder\FieldsBuilder;

$general = new FieldsBuilder('general');

    ->addTab('general', ['placement' => 'left'])
        ->addTrueFalse('enable_social_sharing', ['ui' => 1])
            ->setInstructions('Shows social sharing buttons for various platforms below the title.')
        ->addTrueFalse('enable_featured_image', ['ui' => 1])
            ->setInstructions('Enables automatically displaying the featured image before the content.');
return $general;

Header Tab

We will follow a similar pattern as our general tab above, but renaming $general to $header as well as changing the key we pass to FieldsBuilder():


namespace App;

use StoutLogic\AcfBuilder\FieldsBuilder;

$header = new FieldsBuilder('header');

    ->addTab('header', ['placement' => 'left'])
        ->addText('intro', ['label' => 'Introduction'])
            ->setInstructions('An introduction to be shown before the title.')
            ->setInstructions('A subtitle to be shown after the title.');
return $header;

Creating our Field Groups


For our field groups, we will start by creating fields/post.php which will serve as the base file for any partials we add to our post post type.


namespace App;

use StoutLogic\AcfBuilder\FieldsBuilder;

$post = new FieldsBuilder('post');

    ->setLocation('post_type', '==', 'post');

return $post;


With our pages, we will follow the same pattern as our post fields above, changing instances of post to page inside of fields/page.php. But for pages, perhaps we don’t want the ability to toggle featured images. We are easily able to do this as seen in the example below:


namespace App;

use StoutLogic\AcfBuilder\FieldsBuilder;

$page = new FieldsBuilder('page');

    ->setLocation('post_type', '==', 'page');

return $page;

And voila. Once saved, our field groups are registered and will now show up on the edit screen. In a real-world scenario, you would more than likely have a partial for each post type with perhaps a tab labeled as the name of your post type containing your post type’s custom fields to be included alongside your globally used General and Header tabs.


Another useful thing we could do in an attempt to make this as clean and maintainable as possible is to extract related sets of fields into their own partial. But, instead of a partial, we will call it a component.

Start by creating a components folder inside your fields folder.

While I use components elsewhere, I find them specifically useful with flexible content fields. In this example, we will create a new partial called builder.php inside of our partials folder that could perhaps house a re-usable flexible content field that we end up turning into a page builder of some kind that is usable on specific post types.

Tip: This is just a usage example. An actual page builder, while possible, should be planned a bit more carefully. I will touch on this subject in the future using the methods shown in this guide.

A customizable button can have a multitude of fields such as a label, URL, size, color, etc. This ends up being a lot of fields for just one component of your flexible content, and perhaps you even want to re-use it elsewhere when creating your fields. That being said, it makes a good example:

For our button, we will start by creating button.php inside of our new components folder. A fully customizable button might look something like this:


namespace App;

use StoutLogic\AcfBuilder\FieldsBuilder;

$config = (object) [
    'ui' => 1,
    'wrapper' => ['width' => 30],

$button = new FieldsBuilder('button');


        ->addText('label', ['wrapper' => $config->wrapper])
            ->setInstructions('Label shown on the button.')
        ->addUrl('url', ['label' => 'URL', 'wrapper' => $config->wrapper])
            ->setInstructions('URL for the button to link to')

        ->addSelect('size', ['ui' => $config->ui, 'allow_null' => 1, 'placeholder' => 'Default', 'wrapper' => $config->wrapper])
            ->addChoices(['small' => 'Small'], ['medium' => 'Medium'], ['large' => 'Large'])
            ->setInstructions('The size of the button.')

        ->addSelect('color', ['ui' => $config->ui, 'allow_null' => 1, 'placeholder' => 'None', 'wrapper' => $config->wrapper])
            ->addChoices(['blue' => 'Blue'], ['red' => 'Red'], ['green' => 'Green'])
            ->setInstructions('The background color of the button.')

        ->addTrueFalse('rounded', ['ui' => $config->ui])
            ->setInstructions('Make the button round.')

        ->addTrueFalse('centered', ['ui' => $config->ui])
            ->setInstructions('Center the button horizontally.')

return $button;

You’ll see in my above example the use of $config. When I’m personally working with field groups, I usually create a config object to define various default values that I pass to all of my fields instead of hardcoding them in. This allows a little more control over arguments that I tend to use often. There has been discussion on adding additional global configuration for things like this directly into ACF Builder, but until that happens; this is more or less my personal approach..

Once we have our Button component, similarly to how we added our partials into our post type, we can now do the same thing, but inside of our builder.php partial. Since we are using it for our Flexible Content field, we will use ->addLayout() instead of ->addFields().


namespace App;

use StoutLogic\AcfBuilder\FieldsBuilder;

$builder = new FieldsBuilder('builder');

    ->addTab('builder', ['placement' => 'left'])
        ->addFlexibleContent('components', ['button_label' => 'Add Component'])

return $builder;

Now we could go back to our fields/page.php and plug in our new builder partial:


and that’s it!


The above consists of a lot of examples which are meant to be expanded on for the needs of your project. The folder structure, naming scheme, etc. is opinionated and in the end, entirely up to you.

If you would like to learn more about ACF Builder and it’s configuration, I highly suggest checking out the official Wiki.

If you are looking for a cheat sheet of some kind for different field types and their configuration, I have started one here. PR’s are always welcome. :)

Join the discussion on Roots Discourse

Help support our open-source development efforts

Help us grow

Join over 7,800+ subscribers on our newsletter to get the latest Roots updates, along with occasional tips on building better WordPress sites.

Looking for WordPress plugin recommendations, the newest modern WordPress projects, and general web development tips and articles?

“Easily the best WordPress email I get.” Colin OBrien

Follow us on Twitter @rootswp

Ready to checkout?