Site icon WebDevStudios

Use CMB2 to Create a New Post Submission Form

Here at WebDevStudios, we try to use CMB2 for pretty much anything form or field related.

We do this for several reasons:

  1. It saves us time. It’s a framework that handles a lot of the nitty-gritty for us (creating markup, handling sanitizing, escaping and saving data, etc).
  2. It provides a lot of power. We can build awesome things quicker because CMB2 exists.
  3. We’re eating our own dogfood so that we can continue to hone the tool and make it as helpful as possible (see #1 & #2).

One of the cool and powerful ways we have used CMB2 is for creating front-end submission forms for our users. Traditionally we (and I’m sure many of you) have used Gravity Forms as your go-to front-end form solution. No doubt Gravity Forms is an incredible tool, but we decided for some use-cases, it isn’t the right tool.

For example, if I want to provide a front-end blog post (or other post-type) submission form, I want to avoid Gravity Forms. With a submission form like that, you generally want full control of how it looks, when/where it should display and where the data goes when submitted. You probably don’t want that form created in wp-admin where the site’s owner or users can edit it. Enter CMB2.

I’m going to walk you through how to create a front-end submission form with CMB2.

First step, we need to register our form. This is standard form and field registration:

/**
 * Register the form and fields for our front-end submission form
 */
function wds_frontend_form_register() {
    $cmb = new_cmb2_box( array(
        'id'           => 'front-end-post-form',
        'object_types' => array( 'post' ),
        'hookup'       => false,
        'save_fields'  => false,
    ) );

    $cmb->add_field( array(
        'name'    => __( 'New Post Title', 'wds-post-submit' ),
        'id'      => 'submitted_post_title',
        'type'    => 'text',
        'default' => __( 'New Post', 'wds-post-submit' ),
    ) );

    $cmb->add_field( array(
        'name'    => __( 'New Post Content', 'wds-post-submit' ),
        'id'      => 'submitted_post_content',
        'type'    => 'wysiwyg',
        'options' => array(
            'textarea_rows' => 12,
            'media_buttons' => false,
        ),
    ) );

    $cmb->add_field( array(
        'name'       => __( 'Featured Image for New Post', 'wds-post-submit' ),
        'id'         => 'submitted_post_thumbnail',
        'type'       => 'text',
        'attributes' => array(
            'type' => 'file', // Let's use a standard file upload field
        ),
    ) );

    $cmb->add_field( array(
        'name' => __( 'Your Name', 'wds-post-submit' ),
        'desc' => __( 'Please enter your name for author credit on the new post.', 'wds-post-submit' ),
        'id'   => 'submitted_author_name',
        'type' => 'text',
    ) );

    $cmb->add_field( array(
        'name' => __( 'Your Email', 'wds-post-submit' ),
        'desc' => __( 'Please enter your email so we can contact you if we use your post.', 'wds-post-submit' ),
        'id'   => 'submitted_author_email',
        'type' => 'text_email',
    ) );

}
add_action( 'cmb2_init', 'wds_frontend_form_register' );

As you can see, we’ve used the new CMB2 form and field registration API to register our front-end form and fields. We also used a new parameter from version 2.0.3: 'save_fields' => false. This prevents CMB2 from saving the fields, so we can do what we want with the submission.

We added fields for the post title, post content, and optional fields for the featured image, contributor name, and contributor email. The first item in the 'object_types' array will be used for the new post submission 'post_type'.

Now that we have our form registered, let’s go ahead and register a shortcode handler for our front-end submission form. This adds flexibility as the site owner can place this shortcode anywhere they want. We’ll also allow a few shortcode attributes that we’ll cover later.

/**
 * Handle the cmb-frontend-form shortcode
 *
 * @param  array  $atts Array of shortcode attributes
 * @return string       Form html
 */
function wds_do_frontend_form_submission_shortcode( $atts = array() ) {

    // Current user
    $user_id = get_current_user_id();

    // Use ID of metabox in wds_frontend_form_register
    $metabox_id = 'front-end-post-form';

    // since post ID will not exist yet, just need to pass it something
    $object_id  = 'fake-oject-id';

    // Get CMB2 metabox object
    $cmb = cmb2_get_metabox( $metabox_id, $object_id );

    // Get $cmb object_types
    $post_types = $cmb->prop( 'object_types' );

    // Parse attributes. These shortcode attributes can be optionally overridden.
    $atts = shortcode_atts( array(
        'post_author' => $user_id ? $user_id : 1, // Current user, or admin
        'post_status' => 'pending',
        'post_type'   => reset( $post_types ), // Only use first object_type in array
    ), $atts, 'cmb-frontend-form' );

    // Initiate our output variable
    $output = '';

    // Our CMB2 form stuff goes here

    return $output;
}
add_shortcode( 'cmb-frontend-form', 'wds_do_frontend_form_submission_shortcode' );

So we’ve registered our shortcode, cmb-frontend-form and supplied a callback, wds_do_frontend_form_submission_shortcode. Because our shortcode can accept a few attributes, we’re passing in the $atts array. This will be the array of attributes already parsed out for us by WordPress (if the shortcode has any attributes).

Next, you follow the inline comments to see what is happening.

We are:

  1. Getting the currently logged-in user’s ID (This will be the default post-author user ID for the new post).
  2. Creating our $metabox_id variable. It is the same value as the 'id' parameter we used when registering our metabox/form.
  3. Creating our $object_id variable. This looks hokey, but it allows us to prevent CMB2 from auto-magically finding an object id (which could end up being the post ID of the page the form lives on.. not what we want!). This will ensure that the form remains empty.
  4. Retrieving our $cmb metabox instance by passing the metabox/form id we registered in our first snippet, 'front-end-post-form'.
  5. Retrieving the 'object_types' property from the $cmb object. This object type will be the default 'post_type' for the new post submission (unless overridden by the shortcode attributes).
  6. Parsing the shortcode attributes. We’ll explain in a bit.
  7. Creating an empty $output variable that we’ll be appending (concatenating) too.

To parse the shortcode attributes, we use the shortcode_atts function. This function, according to the Codex, “…Combines user shortcode attributes with known attributes and fills in defaults when needed. The result will contain every key from the known attributes, merged with values from shortcode attributes.”

This allows us to specify some default attributes for our shortcode. In our case, our default 'post_author' for the submitted post will be the currently logged-in user or 1, the default submitted post’s status will be 'pending' and the default submitted post’s 'post_type' will be 'post'. If I wanted, I can change all those attributes by modifying the shortcode in my content like so: [cmb-frontend-form post_author=2 post_status="draft" post_type="page"].

Next, let’s add our CMB2 form to the shortcode output. We’re going to ‘zoom in’ a bit with our snippet. At the end, we’ll put it all together.

/**
 * Handle the cmb-frontend-form shortcode
 *
 * @param  array  $atts Array of shortcode attributes
 * @return string       Form html
 */
function wds_do_frontend_form_submission_shortcode( $atts = array() ) {

    // ... Previous function code omitted for brevity

    // Initiate our output variable
    $output = '';

    // Get our form
    $output .= cmb2_get_metabox_form( $cmb, $object_id, array( 'save_button' => __( 'Submit Post', 'wds-post-submit' ) ) );

    return $output;
}
add_shortcode( 'cmb-frontend-form', 'wds_do_frontend_form_submission_shortcode' );

With that new line, we’re actually retrieving the CMB2 form markup. The parameters we pass to the cmb2_get_metabox_formfunction include the $cmb object we just retrieved, the fake post id we created, and an array of arguments we want to override in the cmb2_get_metabox_form function. In our case, we only want to override the text of the 'save_button' to more accurately reflect what the button will be doing.

So now we have a form on the page, which is pretty exciting. At this point, you may want to take some time to add some custom styles to your theme’s stylesheet to make the form look how you want. But there is a key flaw: the form will not do anything when we submit. Our 'save_fields' => false ensures that. So let’s go ahead and create our submission handler.

/**
 * Handles form submission on save
 *
 * @param  CMB2  $cmb       The CMB2 object
 * @param  array $post_data Array of post-data for new post
 * @return mixed            New post ID if successful
 */
function wds_handle_frontend_new_post_form_submission( $cmb, $post_data = array() ) {

    // If no form submission, bail
    if ( empty( $_POST ) ) {
        return false;
    }

    // check required $_POST variables and security nonce
    if (
        ! isset( $_POST['submit-cmb'], $_POST['object_id'], $_POST[ $cmb->nonce() ] )
        || ! wp_verify_nonce( $_POST[ $cmb->nonce() ], $cmb->nonce() )
    ) {
        return new WP_Error( 'security_fail', __( 'Security check failed.' ) );
    }

    if ( empty( $_POST['submitted_post_title'] ) ) {
        return new WP_Error( 'post_data_missing', __( 'New post requires a title.' ) );
    }

    // Do WordPress insert_post stuff

    return $new_submission_id;
}

Our post-submission handler function, wds_handle_frontend_new_post_form_submission‘ takes two arguments, a CMB2 object, and an optional array of post data for the inserted post.

The first step is to check if the form has even been submitted. If not, we bail out early. If so, then we verify that all the security pieces are in place as well as the required post data. We’re requiring the user to at least submit a title for their post. If all goes well, we’re now ready to create our new post.

Now, let’s leverage a new method, get_sanitized_values, to sanitize the array of fields data submitted, We’ll pass it the $_POSTvariable. Once the values have been properly sanitized, let’s set our new post’s title and content from those fields and insert it!

/**
 * Handles form submission on save
 *
 * @param  CMB2  $cmb       The CMB2 object
 * @param  array $post_data Array of post-data for new post
 * @return mixed            New post ID if successful
 */
function wds_handle_frontend_new_post_form_submission( $cmb, $post_data = array() ) {

    // ... Previous function code omitted for brevity

    // Fetch sanitized values
    $sanitized_values = $cmb->get_sanitized_values( $_POST );

    // Set our post data arguments
    $post_data['post_title']   = $sanitized_values['submitted_post_title'];
    unset( $sanitized_values['submitted_post_title'] );
    $post_data['post_content'] = $sanitized_values['submitted_post_content'];
    unset( $sanitized_values['submitted_post_content'] );

    // Create the new post
    $new_submission_id = wp_insert_post( $post_data, true );

    // If we hit a snag, update the user
    if ( is_wp_error( $new_submission_id ) ) {
        return $new_submission_id;
    }

    return $new_submission_id;
}

Ok, Let’s handle the featured image and custom post meta for the new post:

/**
 * Handles form submission on save
 *
 * @param  CMB2  $cmb       The CMB2 object
 * @param  array $post_data Array of post-data for new post
 * @return mixed            New post ID if successful
 */
function wds_handle_frontend_new_post_form_submission( $cmb, $post_data = array() ) {

    // ... Previous function code omitted for brevity

    // If we hit a snag, update the user
    if ( is_wp_error( $new_submission_id ) ) {
        return $new_submission_id;
    }

    /**
     * Other than post_type and post_status, we want
     * our uploaded attachment post to have the same post-data
     */
    unset( $post_data['post_type'] );
    unset( $post_data['post_status'] );

    // Try to upload the featured image
    $img_id = wds_frontend_form_photo_upload( $new_submission_id, $post_data );

    // If our photo upload was successful, set the featured image
    if ( $img_id && ! is_wp_error( $img_id ) ) {
        set_post_thumbnail( $new_submission_id, $img_id );
    }

    // Loop through remaining (sanitized) data, and save to post-meta
    foreach ( $sanitized_values as $key => $value ) {
        update_post_meta( $new_submission_id, $key, $value );
    }

    return $new_submission_id;
}

You can see we’re using a helper function, wds_frontend_form_photo_upload, (which is just a wrapper for media_handle_upload). It’s not directly related to CMB2, so I won’t go over it here, but I’ll include it in the final code snippet.

After we upload our image to the new post, if all went well, we’ll make that image the post’s featured image (set_post_thumbnail).

And finally, we’ll loop through the rest of the sanitized field values and save them as post meta.

Now that we have our custom save handler, we need to incorporate it back into our shortcode handler function, wds_do_frontend_form_submission_shortcode.

/**
 * Handle the cmb-frontend-form shortcode
 *
 * @param  array  $atts Array of shortcode attributes
 * @return string       Form html
 */
function wds_do_frontend_form_submission_shortcode( $atts = array() ) {

    // ... Previous function code omitted for brevity

    // Initiate our output variable
    $output = '';

    // Handle form saving (if form has been submitted)
    $new_id = wds_handle_frontend_new_post_form_submission( $cmb, $atts );

    if ( $new_id ) {

        if ( is_wp_error( $new_id ) ) {

            // If there was an error with the submission, add it to our ouput.
            $output .= '<h3>' . sprintf( __( 'There was an error in the submission: %s', 'wds-post-submit' ), '<strong>'. $new_id->get_error_message() .'</strong>' ) . '</h3>';

        } else {

            // Get submitter's name
            $name = isset( $_POST['submitted_author_name'] ) && $_POST['submitted_author_name']
                ? ' '. $_POST['submitted_author_name']
                : '';

            // Add notice of submission
            $output .= '<h3>' . sprintf( __( 'Thank you %s, your new post has been submitted and is pending review by a site administrator.', 'wds-post-submit' ), esc_html( $name ) ) . '</h3>';
        }

    }

    // Get our form
    $output .= cmb2_get_metabox_form( $cmb, $object_id, array( 'save_button' => __( 'Submit Post', 'wds-post-submit' ) ) );

    return $output;
}
add_shortcode( 'cmb-frontend-form', 'wds_do_frontend_form_submission_shortcode' );

You can see, right after we intiate our $output variable, and just before we use cmb2_get_metabox_form to retrieve our form markup, we’re using our new wds_handle_frontend_new_post_form_submission function to save any submitted post entries. If it saves the post, it will return the new post ID, but if it hit a snag, will return a WP_Error object.

So if we got a response back from our save handler, we’re going to output a message to the user. If the submission process hit a snag, the user will be alerted with the proper error message (and otherwise be given a ‘success’ message). These messages get appended to the $output variable before the form markup so that they will show up at the top of the form.

One issue with this method is that if user hits refresh after submitting post, a new post will continue to be submitted. You should probably either check for an existing post with the submitted data BEFORE doing wp_insert_post, or implement a redirect when the form is submitted, but I’ll have to cover that another time. (Update 5/23/15: The snippet has been updated to now redirect on successful submission, woot!)

And there you have it! A flexible front-end submission form that you can use to generate new posts (or other post-types). You can use this for all kinds of ideas. Maybe you want a movie review site–just change the registered fields for your form to reflect the kind of data you would want for that review. Title, content, rating, submitter’s name/email, etc. You can see there is a lot of flexibility!

You can find the entire snippet at the CMB2 Snippet Library. If you haven’t yet checked out the CMB2 Snippet Library, you definitely should! There are a lot of tips and tricks (like this one!) throughout.

Exit mobile version