Extending WordPress: Custom Fields with CMB2

Introduction

Custom fields have long been a part of WordPress. They serve to add information to a post or page that does not fit within the standard content editor. They particularly suit structured content; indeed, custom content types may altogether eschew the standard editor and rely entirely on custom fields. Some plugins, such as EDD and WooCommerce, leverage custom fields heavily.

In a work project I was recently asked to implement a way to do custom per-page header text and colours – it made sense to use custom fields for the purpose. The plugin landscape for custom fields includes the well-known Advanced Custom Fields (ACF), Pods, and Piklist, among others. The issue I had with some of these is they allow user-side adding of custom fields, where I only needed a small number of specific fields for pages, preferably coded into the theme for a controlled experience. Enter CMB2.

CMB2 is produced by WebDevStudios, is open source, is available via the plugin repository, and can be used as a library within a WordPress plugin or theme. It is successor to the original CMB project, coming about via complete rewrite. CMB2 also facilitates much more functionality than custom fields, but those are topics for another day.

I wound up using CMB2 to implement the custom fields in the page editor. This post is going to show a simpler version of what I did to satisfy the request, demonstrating how to configure custom fields via CMB2 and how to consume said custom fields from page templates.

There’s a GitHub repository containing the working code demonstrated by this post, if you wish to see the finished result.

Loading CMB2

There are at least a few options for loading CMB2:

  • Use TGMPA to require that the CMB2 plugin be installed
  • Pull CMB2 in via Composer
  • Load it manually as a library

I elected to use use TGMPA to require the CMB2 plugin; I’ve written previously about using TGMPA to load plugin dependencies, so it made sense to use the same technique here. I’m not aware currently of any particular downside of any of the options, except for the possibility of the CMB2 library being loaded multiple times via separate themes / plugins, which apparently is handled gracefully within the library.

For the purpose of this demonstration I created a child theme of the Twenty Fifteen theme, and added in the necessary code for implementing custom fields. There’s first some setup/boilerplate to be done. The child theme’s functions.php resembles:

<?php
require_once get_stylesheet_directory() . '/inc/tgm.php';
require_once get_stylesheet_directory() . '/inc/page-meta.php';

function twenty_fifteen_cmb2_enqueue() {
    $parent_style = 'parent-style'; // This is 'twentyfifteen-style' for the Twenty Fifteen theme.

    // load the parent theme styles
    wp_enqueue_style( $parent_style, get_template_directory_uri() . '/style.css' );

    // load the child theme styles
    wp_enqueue_style( 'child-style',
        get_stylesheet_directory_uri() . '/style.css',
        array( $parent_style ),
        wp_get_theme()->get('Version')
    );
}
add_action( 'wp_enqueue_scripts', 'twenty_fifteen_cmb2_enqueue' );

This snippet is a bare-bones theme initializer, which besides loading up the parent- and child-theme’s respective stylesheets, also brings in two new bits of functionality. The first being the TGMPA configuration (which in turn will require CMB2 to be installed), shown next, and the second being the CMB2 custom fields configuration, covered in the next section.

The child theme stylesheet is currently very bare-bones – it’s just the block comment needed for WordPress to detect and load the theme – so I won’t spend any space on it here.

Check my previous post on TGMPA for details, but the code I used to require CMB2, in /inc/tgm.php, looks like:

<?php
require_once get_stylesheet_directory() . '/lib/tgm/class-tgm-plugin-activation.php';

add_action( 'tgmpa_register', 'twenty_fifteen_cmb2_register_required_plugins' );

function twenty_fifteen_cmb2_register_required_plugins() {
    $plugins = array(
        array(
            'name'      => 'CMB2',
            'slug'      => 'cmb2',
            'required'  => true,
        ),
    );

    $config = array(
        // omitted for brevity
    );

    tgmpa( $plugins, $config );
}

The simplified snippet above is where TGMPA is configured and instructed to require the CMB2 plugin – see the $plugins array inside the twenty_fifteen_cmb2_register_required_plugins function.

That’s the boilerplate out of the way – the theme setup, loading TGMPA, and requiring CMB2. The meat of the post is in the next two sections, respectively on declaring custom fields and consuming custom field values in page templates.

Configuring CMB2

CMB2 custom fields and meta boxes can be set up in a function that runs on the cmb2_init action. A skeleton setup resembles:

<?php
function twenty_fifteen_cmb2_metaboxes() {
}
add_action( 'cmb2_init', 'twenty_fifteen_cmb2_metaboxes' );

With the function ready, a meta box is added using the following minimal configuration:

$cmb_header = new_cmb2_box(
    array(
        'id'           => 'header_settings',
        'title'        => __( 'Header Settings', 'twenty-fifteen-cmb2' ),
        'object_types' => array( 'page' ),
    )
);

The reference to the box instance is put in a variable to add fields later. Each box that is defined this way appears as a selectable option in the Screen Options dropdown tab, along with Custom Fields, Author, etc, and appears in the meta box list below the main editor.

The relevant attributes here are:

  • id – a unique name for the meta box
  • title – defines the visible title for the meta box; it should be set up to be translation-ready, as shown
  • object_types – an array of strings naming the content types (page, post, etc) where the meta box should appear

Available meta box properties are detailed in full on the CMB2 wiki.

With the meta box established, adding a meta field to the box looks like:

$cmb_header->add_field(
    array(
        'name' => __( 'Subtitle', 'twenty-fifteen-cmb2' ),
        'desc' => __( 'A subtitle to show below the page title', 'twenty-fifteen-cmb2' ),
        'id'   => $prefix . 'subtitle',
        'type' => 'text',
    )
);

Notice how the field is added to a box by calling the add_field function on the box reference.

The following attributes are of interest:

  • name – a string for the field label (ideally translation-ready)
  • desc – a string for the field description (optional, displays helper text below the field) (ideally translation-ready)
  • id – a unique name for the field – uniqueness is important as it affects how meta fields are saved and retrieved
  • type – specifies the type of field being added

Numerous field types are built in to CMB2, but more can be created or added via add-on plugins. Built-in field types include textboxes, checkboxes, color pickers, and date/time pickers, though there are many more.

I used the $prefix variable to prepend each field name to ensure the field remains unique to this code, and is not somehow impacted by external code. The leading underscore makes the field private so it is not visible in the Custom Fields meta box; whether this is desirable depends on the intended usage of this code.

Adding a select box looks like the following:

$cmb_header->add_field(
    array(
        'name'    => __( 'Title color', 'twenty-fifteen-cmb2' ),
        'desc'    => __( 'This is the color of the page title' ),
        'id'      => $prefix . 'title_color',
        'type'    => 'select',
        'default' => 'black',
        'options' => array(
            'black'     => __( 'Black', 'twenty-fifteen-cmb2' ),
            'white'     => __( 'White', 'twenty-fifteen-cmb2' ),
            'blue'      => __( 'Blue', 'twenty-fifteen-cmb2' ),
            'darkblue'  => __( 'Dark Blue', 'twenty-fifteen-cmb2' ),
            'green'     => __( 'Green', 'twenty-fifteen-cmb2' ),
            'darkgrey'  => __( 'Dark Grey', 'twenty-fifteen-cmb2' ),
            'lightgrey' => __( 'Light Grey', 'twenty-fifteen-cmb2' ),
            'purple'    => __( 'Purple', 'twenty-fifteen-cmb2' ),
        ),
    )
);

When using a list-based element, the options attribute is used to specify values available. In this case it was desired to be able to choose a specific color for the page title on a per-page basis. I didn’t use a color picker field here because it was desirable to limit the options to a specific selection of colors already in use on the site. Each option defined is a key-value pair, with the key being what appears in the rendered option’s value attribute, and the value is a string, ideally translation-ready as shown. The default attribute should match one of the keys in the options array, and is used to indicate which option should be the default selection.

Putting it together now. The following code sample is lengthy, but it shows how I set up a meta box and added multiple fields within.

<?php
/**
 * Define the metabox and field configurations for pages.
 */
function twenty_fifteen_cmb2_metaboxes() {
    // Start with an underscore to hide fields from custom fields list.
    // Use a unique prefix to reduce chance of naming conflict.
    $prefix = '_2015_cmb2_page_';

    // Initiate the header settings metabox.
    $cmb_header = new_cmb2_box(
        array(
            'id'           => 'header_settings',
            'title'        => __( 'Header Settings', 'twenty-fifteen-cmb2' ),
            'object_types' => array( 'page' ),
        )
    );

    $cmb_header->add_field(
        array(
            'name'    => __( 'Title color', 'twenty-fifteen-cmb2' ),
            'desc'    => __( 'This is the color of the page title' ),
            'id'      => $prefix . 'title_color',
            'type'    => 'select',
            'default' => 'black',
            'options' => array(
                'black'     => __( 'Black', 'twenty-fifteen-cmb2' ),
                'white'     => __( 'White', 'twenty-fifteen-cmb2' ),
                'blue'      => __( 'Blue', 'twenty-fifteen-cmb2' ),
                'darkblue'  => __( 'Dark Blue', 'twenty-fifteen-cmb2' ),
                'green'     => __( 'Green', 'twenty-fifteen-cmb2' ),
                'darkgrey'  => __( 'Dark Grey', 'twenty-fifteen-cmb2' ),
                'lightgrey' => __( 'Light Grey', 'twenty-fifteen-cmb2' ),
                'purple'    => __( 'Purple', 'twenty-fifteen-cmb2' ),
            ),
        )
    );

    $cmb_header->add_field(
        array(
            'name' => __( 'Subtitle', 'twenty-fifteen-cmb2' ),
            'desc' => __( 'A subtitle to show below the page title', 'twenty-fifteen-cmb2' ),
            'id'   => $prefix . 'subtitle',
            'type' => 'text',
        )
    );

    $cmb_header->add_field(
        array(
            'name'    => __( 'Subtitle color', 'twenty-fifteen-cmb2' ),
            'desc'    => __( 'This is the color of the page subtitle' ),
            'id'      => $prefix . 'subtitle_color',
            'type'    => 'select',
            'default' => 'black',
            'options' => array(
                'black'     => __( 'Black', 'twenty-fifteen-cmb2' ),
                'white'     => __( 'White', 'twenty-fifteen-cmb2' ),
                'blue'      => __( 'Blue', 'twenty-fifteen-cmb2' ),
                'darkblue'  => __( 'Dark Blue', 'twenty-fifteen-cmb2' ),
                'green'     => __( 'Green', 'twenty-fifteen-cmb2' ),
                'darkgrey'  => __( 'Dark Grey', 'twenty-fifteen-cmb2' ),
                'lightgrey' => __( 'Light Grey', 'twenty-fifteen-cmb2' ),
                'purple'    => __( 'Purple', 'twenty-fifteen-cmb2' ),
            ),
        )
    );
}
add_action( 'cmb2_init', 'twenty_fifteen_cmb2_metaboxes' );

I incorporated the two fields demonstrated earlier, and one more text color picker field. As before, CMB2 offers a broad selection of fields that can be used here.

Creating or editing a page should yield a result similar to that pictured below, though the textbox is initially blank and the select boxes default to Black.

The custom fields and their containing box are in place. The last part of the solution is to make use of their values in a page template, shown next.

Consuming Custom Fields in Templates

Consuming custom fields is pretty straightforward by use of the get_post_meta function. An example use of it follows:

global $post;
$titleColor = get_post_meta( $post->ID, '_2015_cmb2_page_title_color', true );

The function’s documentation goes into some detail, but in summary, the function takes parameters for the current post ID, the meta field key, and true for retrieving a single value or false for retrieving an array.

It’s possible to retrieve all custom field values for the current post by only passing the post ID, e.g.:

$meta = get_post_meta( $post->ID );
print_r( $meta );

Which would generate the following output:

Array
    (
        [_edit_last] => Array
            (
                [0] => 1
            )

        [_edit_lock] => Array
            (
                [0] => 1502928313:1
            )

        [_thumbnail_id] => Array
            (
                [0] => 5
            )

        [_2015_cmb2_page_title_color] => Array
            (
                [0] => black
            )

        [_2015_cmb2_page_subtitle] => Array
            (
                [0] => Some non-empty subtitle text
            )

        [_2015_cmb2_page_subtitle_color] => Array
            (
                [0] => black
            )
    )

The first few entries are standard meta fields that WordPress core implements. The rest are the ones populated by the previously defined custom fields.

This output can be useful in development for verifying that the field name and values are being saved as expected. Given the relevant array keys listed, it’s simple to retrieve the custom field values:

$titleColor    = get_post_meta( $post->ID, '_2015_cmb2_page_title_color',    true );
$subtitle      = get_post_meta( $post->ID, '_2015_cmb2_page_subtitle',       true );
$subtitleColor = get_post_meta( $post->ID, '_2015_cmb2_page_subtitle_color', true );

Note that the function may return an empty result equivalent to false, so it’s important to check returned values are before usage. Can be done like:

if ( ! empty( $subtitle ) ) {
    // value was saved
}

// OR

if ( $subtitle ) {
    // value was saved
}

I copied the content-page.php template from the Twenty Fifteen theme and made my changes. The updated template follows, edited for brevity:

<?php
global $post;

$titleColor    = get_post_meta( $post->ID, '_2015_cmb2_page_title_color',    true );
$subtitle      = get_post_meta( $post->ID, '_2015_cmb2_page_subtitle',       true );
$subtitleColor = get_post_meta( $post->ID, '_2015_cmb2_page_subtitle_color', true );

// The pre-defined possible color values - a whitelist.
$colors = array( 'black', 'white', 'blue', 'darkblue', 'green', 'darkgrey', 'lightgrey', 'purple' );

// Establish custom classes for the content wrapper.
$classes = array();

// If valid colors are selected, add custom classes to the page container element.
// If value is invalid, no custom color will be applied, and fall back to stylesheet color.

if ( in_array( $titleColor, $colors, true ) ) : // Checking page title color selection
    $classes[] = sprintf( 'custom-title-color-%s', $titleColor );
endif;

if ( in_array( $subtitleColor, $colors, true ) ) : // Checking page subtitle color selection
    $classes[] = sprintf( 'custom-subtitle-color-%s', $subtitleColor );
endif;
?>

<article id="post-<?php the_ID(); ?>" <?php post_class( $classes ); ?>>
    <header class="entry-header">
        <?php the_title( '<h1 class="entry-title">', '</h1>' ); ?>
        <?php if ( ! empty( $subtitle ) ) : // Ensure there is a value before adding markup. ?>
            <p class="entry-subtitle"><?php echo esc_html( $subtitle ); ?></p>
        <?php endif; ?>
    </header><!-- .entry-header -->
</article><!-- #post-## -->

Note the <?php post_class( $classes ); ?> in the article element. Calling post_classes() with an array of strings will cause each string to be added to the set of classes for the containing element, in this case the article. This now requires an addition to the child theme stylesheet:

.custom-title-color-black .entry-title,
.custom-subtitle-color-black .entry-subtitle { color: black }

.custom-title-color-white .entry-title,
.custom-subtitle-color-white .entry-subtitle { color: white }

.custom-title-color-blue .entry-title,
.custom-subtitle-color-blue .entry-subtitle { color: blue }

.custom-title-color-darkblue .entry-title,
.custom-subtitle-color-darkblue .entry-subtitle { color: darkblue }

.custom-title-color-green .entry-title,
.custom-subtitle-color-green .entry-subtitle { color: green }

.custom-title-color-lightgrey .entry-title,
.custom-subtitle-color-lightgrey .entry-subtitle { color: lightgray }

.custom-title-color-darkgrey .entry-title,
.custom-subtitle-color-darkgrey .entry-subtitle { color: darkgray }

.custom-title-color-purple .entry-title,
.custom-subtitle-color-purple .entry-subtitle { color: purple }

Each pair of selectors corresponds to a single whitelisted color, matching the colors in the code sample above and in the select list definitions earlier. This does require coordinating a list of colors in three places, but it also limits selection to a specific set of colors to ensure readability and consistency. The colors themselves can be whatever they are needed to be, and in any number needed – the goal is to guide and simplify color selection on the part of the WordPress user.

The final result:

It’s not flashy, but it does reflect the custom subtitle and title/subtitle colors selected in the previous image of the page editing screen.

Conclusion

As stated early on, CMB2 can be leveraged for much more than just custom fields. Other uses include user fields, front-end forms, options pages, and shortcode-generating modals. Not only that, due to CMB2’s extensible design, it can make use of external custom field types for greater versatility.

But this post was about implementing custom fields using CMB2. Inspired by a work project, I demonstrated how to define a few custom fields, and how to make use of said fields’ values within a page template. This example is extensible to include other content types or custom field types, or to add custom logic such as that for whitelisting colors. It take a bit of orchestration to set up, but the big upside is a controlled and guided editing experience for users.

As before, the full working example plugin is available on GitHub.

Further Reading