acf-builder
acf-builder copied to clipboard
How are you using it?
Before anything else, thank you very much for this awesome lib, it really prevents a lot of headaches ❤️ .
I was wondering about how people are using acf-builder
. Where the fields and (possibly) partial fields are located and where are they loaded?
My current folder structure is heavily inspired by Sage. I have a app/fields
directory inside my theme with each of its files returning one or more FieldBuilders
. The script in charge of loading (acf_add_local_field_group
) the fields is inside the setup.php
(part of my functions.php
)
Folder structure:
├── app/ # → Theme PHP
│ ├── fields/ # → ACF Fields
│ ├── fields/partials/ # → Partial ACF fields, for include/require
│ └── setup.php # → Read and load all fields in 'app/fields'
setup.php
/**
* ACF Builder initialization and fields loading
*/
define('FIELDS_DIR', dirname(__FILE__) . '/fields');
if (is_dir(FIELDS_DIR)) {
add_action('acf/init', function () {
foreach (glob(FIELDS_DIR . '/*.php') as $file_path) {
if (($fields = require_once $file_path) !== true) {
if (!is_array($fields)) {
$fields = [$fields];
}
foreach ($fields as $field) {
if ($field instanceof FieldsBuilder) {
acf_add_local_field_group($field->build());
}
}
}
}
});
}
Sample field app/fields/sample.php
$sample_field = new FieldsBuilder('sample_field');
$sample_field
->addText('title')
->addWysiwyg('content')
->setLocation('post_type', '==', 'page')
->or('post_type', '==', 'post');
return $sample_field;
Note: I haven't tested this a lot and probably the hook can be optimized (maybe switching from glob()
to scandir()
?).
@kaisermann great question. You're implementation looks clean with having each FieldsBuilder in its own file. Do you find yourself using other using the ->addFields
function to take fields of one FieldsBuilder to add them to another? How do you handle that?
If you're using Sage / Bedrock you might be able to easily turn these into classes, and take advantage of composer's autoloader.
I originally started working on another library and extracted ACF Builder out of it. So I don't use ACF Builder by itself per se. Here is an example https://github.com/StoutLogic/understory-acf/wiki
A bit of background: Understory is an Model / View / ViewModel like library that wraps around Timber and Twig. Post Types, Views (actually a View Model for the twig template, and actual PHP file loaded by WordPress) and Taxonomies are defined as classes where you can define custom functions to perform any business logic, or plumbing logic, etc in order to keep the actual Twig template super clean. Timber is great by itself but I find that it can become unruly as complex logic tends to pollute the twig template. And if you do it in the PHP file (WordPress PHP template) you end up with a lot of repeating boilerplate.
Understory\ACF is the ACF extension, where you can create FieldGroup classes. These classes are configured with ACF Builder. You can add any logic, and then easily associate them with Post types, views, taxonomies (which will set the field group's location appropriately and register them) and use those functions in your twig templates. No more get_field
functions littered everywhere.
I would love to put more time into the core Understory and Understory\ACF libraries for everyone to use but they don't have much if any test coverage and the API is very volatile. Also free time is a limiting factor.
Wow, thanks for the complete answer! I didn't test my setup a lot, so for now using one FieldsBuilder in another is done by separating "main" FieldsGroups (the ones that are actually read by the hook) and "partials" (inside the partials directory, which are not loaded). Then I just use the returned value from include FIELDS_DIR . '/partials/partial-field.php'
.
I'm definitely going to take a look at understory-acf!
add_action('init', function () {
$fields = glob(config('theme.dir').'/app/{fields,fields/partials}/*.php', GLOB_BRACE);
array_map(function ($field) {
if ($fields = require_once($field)) {
if (is_array($fields)) {
array_map(function ($field) {
if ($field instanceof FieldsBuilder) {
return acf_add_local_field_group($field->build());
}
}, $fields);
}
return acf_add_local_field_group($fields->build());
};
}, $fields);
}, 2);
@kaisermann I did an array_map()
version of your field loader. Not sure if it's faster than foreach
for this use-case but I've heard it is in many others. Unsure if there's any other tricks to simplify this further.
Unfortunately, glob()
doesn't let me use a wildcard for the directory before the file so I was forced to use GLOB_BRACE
...
I also use init
instead of acf/init
as I've had issues with it in the past firing too quickly and not returning values for get_term()
, etc. that I've needed for making fields dynamically and init
works without an issue.
Edit Just read your reply above that you weren't autoloading partials. That seems sane.
I'm unsure how you do multiple fields in a single field.php
though? Aren't we only getting the return value? Is it possible to chain them?
Otherwise, we could probably scrap the second array_map()
in my attempt above. But like you, I'm still unsure how to make this as clean as possible. 🦄
In my use-case, I'm contemplating just firing acf_add_local_field_group()
in my files and making them class-based.
For that, this would probably suffice:
/**
* Register Fields
*/
add_action('init', function () {
$fields = glob(config('theme.dir').'/app/{fields,fields/partials}/*.php', GLOB_BRACE);
array_map(function ($field) {
if (!require_once($field)) {
wp_die(sprintf(__('Error locating <code>%s</code> for inclusion.', 'app'), $field));
}
}, $fields);
}, 2);
Thought I'd also say how I'm currently implementing it is simply having a metabox.php
for post/page/etc., widgets.php
for my ACF-powered sidebar widgets, options.php
for my theme options, and then everything is using classes such as:
/**
* Initialize Options
*/
if (!class_exists('Options')) {
class Options
{
/**
* Constructor
*/
public function __construct()
{
// Initialize FieldsBuilder
$this->options = new FieldsBuilder('theme_options', [
'style' => 'seamless'
]);
// Settings
$this->settings = [
'ui' => 1,
'wrapper' => ['width' => 15],
'ip' => $_SERVER['REMOTE_ADDR'] ?? ''
];
add_action('init', function() {
$page = isset($_GET['page']) ? $_GET['page'] : '';
if ($page == 'acf-options-theme-options') {
add_filter('screen_options_show_screen', '__return_false');
add_filter('update_footer', '__return_empty_string', 11);
}
});
add_action('init', function() {
acf_add_options_page([
'page_title' => get_bloginfo('name'),
'menu_title' => 'Theme Options',
'capability' => 'edit_theme_options',
'position' => '999',
'post_id' => 'options',
'autoload' => true
]);
// Fields
$this->__general();
// Build
acf_add_local_field_group($this->options->build());
});
}
protected function __general()
{
[...]
}
}
}
but I indeed long for a cleaner approach all around with the ability to easily re-use partials
, etc.
I'd happily take another theme dependency to have a loader for ACF Builder that works similar to Sage 9's controller that lets us include and make use of partials as trait
s or something. 😕
Thought I'd update with a small rewrite of how I'm registering fields in Sage 9 utilizing collect()
:
/**
* 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) {
acf_add_local_field_group($field->build());
}
});
}, 12);
Hey guys!
We built a library around ACF Builder. We've used it on 4-5 sites so far and the results are amazing - development time is much faster and the resulting code is super clean, even when built by our front-end devs who generally don't spend a lot of time specifically on PHP/WP dev. I haven't had a chance to check out understory-acf yet, but looks like we're solving kind-of similar problems.
It's not "ready" yet, but I noticed this discussion and decided to publish it anyway - maybe it helps someone. Take a look - https://github.com/codelight-eu/acf-blocks/
Steve, thank you so much for ACF Builder!
@indrek-k Wow thats awesome. I'm humbled that people use the library at all, let alone make it a dependency of another library :)
This does look to solve the same problem as my other project, which is getting data out of ACF and doing something with it in a modular, clean and repeatable way. Having WordPress templates littered with business logic and plumbing code is a nightmare to maintain. Glad I'm not the only one searching for a better way.
Have you ever checked out Timber? I find it helps maintain a good level a separation of business / presentation logic as well as provides powerful and modular templating with twig. It could pair well with what you're doing. I use it (and extend it) in all of my client projects.