acf-builder icon indicating copy to clipboard operation
acf-builder copied to clipboard

How are you using it?

Open kaisermann opened this issue 7 years ago • 7 comments

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 avatar Aug 04 '17 21:08 kaisermann

@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.

stevep avatar Aug 07 '17 15:08 stevep

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!

kaisermann avatar Aug 07 '17 17:08 kaisermann

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);

Log1x avatar Sep 16 '17 04:09 Log1x

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 traits or something. 😕

Log1x avatar Sep 16 '17 04:09 Log1x

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);

Log1x avatar Jan 09 '18 21:01 Log1x

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 avatar Jan 10 '18 03:01 indrek-k

@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.

stevep avatar Jan 16 '18 04:01 stevep