Making Better WordPress Plugins with ACF

If you’ve read my How to Write a WordPress Plugin article, you know that I’m fairly opinionated on how plugins should be built. The aforementioned article does a fairly deep dive on where to put various parts of a custom plugin using an example problem of needing to create a custom post type and taxonomy. In this article I plan to expand on the concepts of my previous article but use Advanced Custom Fields.

What is Advanced Custom Fields?

Advanced Custom Fields, or ACF, is a plugin that allows you to define the meta fields and interfaces for your Custom Post Types and Taxonomies graphically in the WordPress admin. It has both a free and a premium version. The free version is powerful and fairly full featured. However, some CPTs will require more premium field types that are available as separate add-ons or in the Pro version of the plugin. Both pro and community versions of the plugin allow and encourage you to package ACF in your theme or plugin making it ideal for creating top-notch interfaces without a ton of work.

Quick break down of why I suggest a pro license purchase

The pro version of the ACF plugin comes with the following add-ons built-in:

  • The Repeater Field: Allows for a variable length grouping of fields.
  • The Gallery Field: A straight forward and drop in gallery component.
  • The Flexible Content Field: A component that allows for creation of layouts by use of preconfigured content modules. Think of it as a more flexible repeater field.
  • The Option Pages add-on allows you to build out settings/options pages visually.

Using ACF means you don’t have to re-invent the wheel when it comes to creating thought out and usable interfaces for your plugin. As a plugin developer, ACF Pro adds even more value.

Use ACF for adding custom post types to my plugin

First of all, lets talk plugin setup

Take the following directory structure:

  • wp-plugin-name/
    • wp-plugin-name.php –main plugin class
    • includes/ –define each post type separately in this directory.
      • advanced-custom-fields-pro/ –this article assumes ACF pro is in this folder
      • example-post-type.php –each post type is defined separately.
      • settings.php –this is the plugin settings page template.

The main plugin file

The main plugin file exists to boostrap the plugin. It does so by providing activation/deactivation hooks, including ACF, and then creating instances of the plugin’s entities: settings and example post type.

<?php
/**
 * @package Custom Plugin utilizing ACF for WordPress
 * @version 1.0
 */
/*
Plugin Name: Custom Plugin utilizing ACF for WordPress
Plugin URI: http://www.yaconiello.com/blog/making-better-wordpress-plugins/
Description: This plugin attempts to illustrate using clear exampes how to leverage ACF's power in a custom plugin.
Author: Francis Yaconiello
Version: 1.0
Author URI: http://www.yaconiello.com/
*/

if(!class_exists("CustomPlugin"))
{
    /**
     * class:   CustomPlugin
     * desc:    plugin class to allow reports be pulled from multipe GA accounts
     */
    class CustomPlugin
    {
        /**
         * Created an instance of the CustomPlugin class
         */
        public function __construct()
        {
            // Set up ACF
            add_filter('acf/settings/path', function() {
                return sprintf("%s/includes/advanced-custom-fields-pro/", dirname(__FILE__));
            });
            add_filter('acf/settings/dir', function() {
                return sprintf("%s/includes/advanced-custom-fields-pro/", plugin_dir_url(__FILE__));
            });
            require_once(sprintf("%s/includes/advanced-custom-fields-pro/acf.php", dirname(__FILE__)));

            // Settings managed via ACF
            require_once(sprintf("%s/includes/settings.php", dirname(__FILE__)));
            $settings = new CustomPlugin_Settings(plugin_basename(__FILE__));

            // CPT for example post type
            require_once(sprintf("%s/includes/example-post-type.php", dirname(__FILE__)));
            $exampleposttype = new CustomPlugin_ExamplePostType();
        } // END public function __construct()

        /**
         * Hook into the WordPress activate hook
         */
        public static function activate()
        {
            // Do something
        }

        /**
         * Hook into the WordPress deactivate hook
         */
        public static function deactivate()
        {
            // Do something
        }
    } // END class CustomPlugin
} // END if(!class_exists("CustomPlugin"))

if(class_exists('CustomPlugin'))
{    
    // Installation and uninstallation hooks
    register_activation_hook(__FILE__, array('CustomPlugin', 'activate'));
    register_deactivation_hook(__FILE__, array('CustomPlugin', 'deactivate'));
    
    // instantiate the plugin class
    $plugin = new CustomPlugin();
} // END if(class_exists('CustomPlugin'))

So whats going on here?

  • The file starts with a standard WordPress plugin docblock
  • A class definition for CustomPlugin follows the docblock
  • Next, the activatation and deactivation hooks are defined
  • Lastly, the plugin class is instantiated. In the constructor:
    • ACF is set up as per the instruction on the ACF website. Note that this is referencing the pro version of ACF copied into my plugin at the includes/acf path.
    • my CustomPlugin_Settings class is included and instantiated
    • my CustomPlugin_ExamplePostType class is included and instantiated
    • Note: CustomPlugin_Settings and CustomPlugin_ExamplePostType rely on ACF so they are included and called after ACF.

Go ahead and stub out both CustomPlugin_Settings and CustomPlugin_ExamplePostType in their respective files so that the plugin can be activated through the WordPress admin.

includes/settings.php

<?php
if(!class_exists('CustomPlugin_Settings'))
{
    class CustomPlugin_Settings
    {
    } // END class CustomPlugin_Settings
} // END if(!class_exists('CustomPlugin_Settings'))

includes/example-post-type.php

<?php
if(!class_exists('CustomPlugin_ExamplePostType'))
{
    class CustomPlugin_ExamplePostType
    {
    } // END class CustomPlugin_ExamplePostType
} // END if(!class_exists('CustomPlugin_ExamplePostType'))

The Settings page

So here is where the real magic starts. We are going to flesh out the CustomPlugin_Settings class.

<?php
if(!class_exists('CustomPlugin_Settings'))
{
    class CustomPlugin_Settings
    {
        const SLUG = "custom-plugin-options";

        /**
         * Construct the plugin object
         */
        public function __construct($plugin)
        {
            // register actions
            acf_add_options_page(array(
                'page_title' => __('Custom Plugin', 'custom'),
                'menu_title' => __('Custom Plugin', 'custom'),
                'menu_slug' => self::SLUG,
                'capability' => 'manage_options',
                'redirect' => false
            ));

            add_action('init', array(&$this, "init"));
            add_action('admin_menu', array(&$this, 'admin_menu'), 20);
            add_filter("plugin_action_links_$plugin", array(&$this, 'plugin_settings_link'));
        } // END public function __construct
        
        /**
         * Add options page
         */
        public function admin_menu()
        {
            // Duplicate link into properties mgmt
            add_submenu_page(
                self::SLUG,
                __('Settings', 'custom'),
                __('Settings', 'custom'),
                'manage_options',
                self::SLUG,
                1
            );
        }

        /**
         * Add settings fields via ACF
         */
        function init()
        {
            if(function_exists('register_field_group'))
            {
                // OUR ACF GOODIES ARE GOING TO GO HERE
            }
        }

        /**
         * Add the settings link to the plugins page
         */
        public function plugin_settings_link($links)
        { 
            $settings_link = sprintf('<a href="admin.php?page=%s">Settings</a>', self::SLUG); 
            array_unshift($links, $settings_link); 
            return $links; 
        } // END public function plugin_settings_link($links)
    } // END class CustomPlugin_Settings
} // END if(!class_exists('CustomPlugin_Settings'))

So whats going on here?

  • First we add a class constant named simply SLUG this constant will store the slug used for this page in WordPress admin.
  • Next we add a constructor that does the following:
  • the public method admin_menu adds a submenu page with the title Settings to the sidebar under the Custom Plugin menu created in the constructor
  • the public method init is a stub that will contain the fields for the settings page created via ACF - more on that later
  • the public method plugin_settings_link adds a Settings link to the plugin listing next the deactivate button
    Settings Link

Ok I have a settings file. What now?

The next step is you using the ACF interface to create a settings page. If your plugin is not activated, do so. This will unveil a sidebar menu option Custom Fields. Go into this new menu and create a custom field for your options page.

ACF Option Page

Notice that what I named my Option Page in the page_title attribute of the acf_add_options_page call is selected in the Option Page is equal to X formula. This is me saying that All of the fields I’m about to build out are supposed to show only on that one page.

ACF Repeater Field

Next, lets add some fields to that settings page. Click the Add Field button. I’m going to add a repeater field with two sub fields Value and Label.

ACF Field Export

After saving/publishing your Option Page Custom Fields go to the Import/Export option in the Custom Fields sidebar. Check the appropriate checkbox for the Option Page and click Generate Export Code.

This should give you a screen that has a code printout that can be copied.

ACF Field Export Copy

Copy the code from the step above and replace the contents of your CustomPlugin_Settings’s init function. Note: I have cleaned up the tabbing, code bracketing, and utilized the self::SLUG class constant in the ACF configuration near the bottom.

/**
 * Add settings fields via ACF
 */
public function init()
{
    if(function_exists('register_field_group'))
    {
        register_field_group(array (
            'key' => 'group_5537be89e7c6b',
            'title' => 'Custom Plugin Settings',
            'fields' => array (
                array (
                    'key' => 'field_5537f93805d5b',
                    'label' => 'Some Repeating Option',
                    'name' => 'some_repeating_option',
                    'prefix' => '',
                    'type' => 'repeater',
                    'instructions' => '',
                    'required' => 1,
                    'conditional_logic' => 0,
                    'wrapper' => array (
                        'width' => '',
                        'class' => '',
                        'id' => '',
                    ),
                    'min' => '',
                    'max' => '',
                    'layout' => 'table',
                    'button_label' => 'Add Row',
                    'sub_fields' => array (
                        array (
                            'key' => 'field_5537f95905d5c',
                            'label' => 'Value',
                            'name' => 'value',
                            'prefix' => '',
                            'type' => 'text',
                            'instructions' => '',
                            'required' => 1,
                            'conditional_logic' => 0,
                            'wrapper' => array (
                                'width' => '',
                                'class' => '',
                                'id' => '',
                            ),
                            'default_value' => '',
                            'placeholder' => '',
                            'prepend' => '',
                            'append' => '',
                            'maxlength' => '',
                            'readonly' => 0,
                            'disabled' => 0,
                        ),
                        array (
                            'key' => 'field_5537f96e05d5d',
                            'label' => 'Label',
                            'name' => 'label',
                            'prefix' => '',
                            'type' => 'text',
                            'instructions' => '',
                            'required' => 1,
                            'conditional_logic' => 0,
                            'wrapper' => array (
                                'width' => '',
                                'class' => '',
                                'id' => '',
                            ),
                            'default_value' => '',
                            'placeholder' => '',
                            'prepend' => '',
                            'append' => '',
                            'maxlength' => '',
                            'readonly' => 0,
                            'disabled' => 0,
                        ),
                    ),
                ),
            ),
            'location' => array (
                array (
                    array (
                        'param' => 'options_page',
                        'operator' => '==',
                        'value' => self::SLUG,
                    ),
                ),
            ),
            'menu_order' => 0,
            'position' => 'normal',
            'style' => 'default',
            'label_placement' => 'top',
            'instruction_placement' => 'label',
            'hide_on_screen' => '',
        ));
    }
}

At this point when you go to your settings page you should see something like this:

Settings page all did up

Note: I’ve filled in some color values as an example. Note 2: Your configuration for this page is exported to code and activated via the plugin. You can trash your working copy from the Custom Fields interface and the Options panel will still work. Note 3: Just because you can doesn’t mean you should. On your development machine keep a copy of the Custom Field in the ACF interface for later modification.

The Custom Post Type File

Replace the contents of the example-post-type.php file with the following.

<?php
/**
 * Add a Custom Post Type: example
 */
if(!class_exists('CustomPlugin_ExamplePostType'))
{
    class CustomPlugin_ExamplePostType
    {
        const SLUG = "example";

        /**
         * Construct the custom post type for Reports
         */
        public function __construct()
        {
            // register actions
            add_action('init', array(&$this, 'init'));
        } // END public function __construct()
        
        /**
         * Hook into the init action
         */
        public function init()
        {
            // Register the Analytics Report post type
            register_post_type(self::SLUG,
                array(
                    'labels' => array(
                        'name' => __(sprintf('%ss', ucwords(str_replace("_", " ", self::SLUG))), 'custom'),
                        'singular_name' => __(ucwords(str_replace("_", " ", self::SLUG)), 'custom')
                    ),
                    'description' => __("Example post type", 'custom'),
                    'supports' => array(
                        'title',
                    ),
                    'public' => true,
                    'show_ui' => true,
                    'has_archive' => true,
                    'show_in_menu' => CustomPlugin_Settings::SLUG,
                )
            );

            if(function_exists("register_field_group"))
            {
                // Our ACF Goodies are going to go here
            } // END if(function_exists("register_field_group"))
        } // END public function init()
    } // END class CustomPlugin_ExamplePostType
} // END if(!class_exists('CustomPlugin_ExamplePostType'))

Again this class is really straight forward, ACF is going to do all the work for you.

  • it has a class constant SLUG that represent’s this unique post type’s slug
  • it has a constructor that simply hooks into the WordPress init action on instantiation
  • and it has an public init method that:
    • calls register_post_type
    • has a holding block for the ACF configuration we will create and export to code in the next step

Create a new ACF Field Group, make sure that it is restricted to a post type of Example.

Example CPT ACF

The init function looks like this after including the exported Custom Field Group.

/**
 * Hook into the init action
 */
public function init()
{
    // Register the Analytics Report post type
    register_post_type(self::SLUG,
        array(
            'labels' => array(
                'name' => __(sprintf('%ss', ucwords(str_replace("_", " ", self::SLUG))), 'custom'),
                'singular_name' => __(ucwords(str_replace("_", " ", self::SLUG)), 'custom')
            ),
            'description' => __("Example post type", 'custom'),
            'supports' => array(
                'title',
            ),
            'public' => true,
            'show_ui' => true,
            'has_archive' => true,
            'show_in_menu' => CustomPlugin_Settings::SLUG,
        )
    );

    if(function_exists("register_field_group"))
    {
        register_field_group(array (
            'key' => 'group_5538080de1ea3',
            'title' => 'Example Post Type',
            'fields' => array (
                array (
                    'key' => 'field_5538081dbcdb2',
                    'label' => 'Test Field',
                    'name' => 'test_field',
                    'prefix' => '',
                    'type' => 'text',
                    'instructions' => '',
                    'required' => 1,
                    'conditional_logic' => 0,
                    'wrapper' => array (
                        'width' => '',
                        'class' => '',
                        'id' => '',
                    ),
                    'default_value' => '',
                    'placeholder' => '',
                    'prepend' => '',
                    'append' => '',
                    'maxlength' => '',
                    'readonly' => 0,
                    'disabled' => 0,
                ),
            ),
            'location' => array (
                array (
                    array (
                        'param' => 'post_type',
                        'operator' => '==',
                        'value' => self::SLUG,
                    ),
                ),
            ),
            'menu_order' => 0,
            'position' => 'normal',
            'style' => 'default',
            'label_placement' => 'top',
            'instruction_placement' => 'label',
            'hide_on_screen' => '',
        ));

    } // END if(function_exists("register_field_group"))
} // END public function init()

When you go to the Custom Plugin admin panel sidebar you should see a new subpage options Example and Settings.

Example CPT ACF

At a basic level this is how you can build custom plugins with Advanced Custom Fields.

Written on April 21, 2015