Skip to content

Advanced Custom Fields

Lumbermill uses Advanced Custom Fields for all WordPress admin field definitions. Fields are written in PHP using the ACF Builder library and live in the acf-php/ directory.

All ACF field groups are located in acf-php/. Each subdirectory maps to a part of WordPress — pages, templates, post types, and so on.

Start by creating the file structure for your field group. For a header group on a news-overview template:

  • Directorylumbermill
    • Directoryacf-php
      • Directorytemplates
        • Directorynews-overview
          • index.php
          • header.php

Open header.php and define the field group:

acf-php/templates/news-overview/header.php
<?php
return createFieldGroup('News Overview - Header', function ($fieldGroup) {
// fields go here
});

Add fields to the group by calling addField() with the appropriate field type:

acf-php/templates/news-overview/header.php
<?php
use App\Lib\ACF\Field\AdvancedLink;
use JorenRothman\ACFBuilder\Fields\Basic\Text;
use JorenRothman\ACFBuilder\Fields\Content\Image;
use JorenRothman\ACFBuilder\Fields\Content\Wysiwyg;
return createFieldGroup('News Overview - Header', function ($fieldGroup) {
$image = Image::make('Image');
$title = Text::make('Title');
$text = Wysiwyg::make('Text');
$link = AdvancedLink::make('Link');
$fieldGroup->addField($image);
$fieldGroup->addField($title);
$fieldGroup->addField($text);
$fieldGroup->addField($link);
});

Open news-overview/index.php and register the group with a location rule:

acf-php/templates/news-overview/index.php
<?php
use JorenRothman\ACFBuilder\FieldGroupLocations;
$header = loadFieldGroup(__DIR__ . '/header.php');
$location = new FieldGroupLocations;
$location->and('page_template', '==', 'templates/news-overview.php');
$header->register($location, 10);

That’s it — no wiring step. acf-php/index.php autoloads every nested index.php with a glob, so a new templates/news-overview/index.php is picked up automatically:

acf-php/index.php
foreach (glob(__DIR__ . '/*/*/index.php') as $fieldGroup) {
include_once $fieldGroup;
}

There are three ways to fetch ACF fields: via a Timber object in PHP, from an option page, or directly in a Twig template.

Get the post object and call meta() to fetch fields. Group related fields under a single context key to keep templates clean and avoid naming conflicts:

single.php
$post = Timber::get_post();
$context['header'] = [
'title' => $post->meta('header_title'),
'text' => $post->meta('header_text'),
'image' => $post->meta('header_image'),
];

Access the values in Twig using dot notation:

{# views/templates/single.twig #}
<div class="header">
<h1>{{ header.title }}</h1>
<p>{{ header.text }}</p>
<img src="{{ header.image.src }}" alt="{{ header.image.alt }}" />
</div>

Timber automatically transforms certain ACF field types — relationship fields become Timber\Post objects, image fields become Timber\Image objects, and so on. See Getting data from ACF for the full list.

Option page fields hold global site settings. Always fetch them via ACFFieldsUtil::getOptions() — it calls get_fields('options') once per request and caches the result, so multiple callers share a single DB query.

The preferred place to expose option values globally is config/timber.php — see the Context guide for details. You may also fetch them per-template:

a-file.php
<?php
use App\Util\ACFFieldsUtil;
use Timber\Timber;
$context = Timber::context();
$post = Timber::get_post();
$context['post'] = $post;
$options = ACFFieldsUtil::getOptions();
$context['options_page_option'] = $options['field_name'] ?? null;
Timber::render('templates/a-view-file.twig', $context);

If the field stores raw HTML or script tags, use getRawOptions() instead — getOptions() applies ACF formatting which can corrupt raw HTML output:

$options = ACFFieldsUtil::getRawOptions();
echo $options['my_script_field'] ?? '';

Meta fields may be fetched directly in Twig:

{# a-file.twig #}
{{ post.meta('field_name') }}