How to validate custom fields in custom post type?

I wrote a plugin that creates a custom post type with custom fields. To prevent users from entering incorrect information, how can I validate the data?

I assumed that the save_post hook function would process field validation, but I can’t seem to find a straight forward way that displays errors back to the user.

Read More

Is there a built-in wordpress function/feature for this? What is the general technique for accomplishing custom field validation?

Related posts

Leave a Reply

5 comments

  1. You’re on the right track. I test the fields in the save_post callback, and then use admin notices to display errors to the user when a field fails validation. They show up just in a highlighted box at the top of the page, just like any errors/messages that WordPress itself generates.

    Here’s a simple example of creating an admin notice:

    function my_admin_notice()
    {
        ?>
    
        <div class="updated">
           <p>Aenean eros ante, porta commodo lacinia.</p>
        </div>
    
        <?php
    }
    add_action( 'admin_notices', 'my_admin_notice' );
    

    That’s not very practical, though. In a situation like this, you really just want a function that you can pass a message to. Something like,

    if( $pizza != 'warm' )
        $notices->enqueue( 'Pizza is not warm', 'error' );
    

    So, you can write that enqueue() function yourself (along with a function to print the notices), or you can include a library like IDAdminNotices.

    Here’s an example from a plugin I wrote. This uses notice enqueue/print functions that are built into the class itself, rather than including an external library.

    public function saveCustomFields( $postID )
    {
        // ...
    
        if( filter_var( $_POST[ self::PREFIX . 'zIndex'], FILTER_VALIDATE_INT ) === FALSE )
        {
            update_post_meta( $post->ID, self::PREFIX . 'zIndex', 0 );
            $this->enqueueMessage( 'The stacking order has to be an integer.', 'error' );
        }   
        else
            update_post_meta( $post->ID, self::PREFIX . 'zIndex', $_POST[ self::PREFIX . 'zIndex'] );
    
        // ...
    }
    add_action( 'save_post',    array( $this, 'saveCustomFields' );
    
  2. I wrote a small plugin that not only validates the input fields on custom post types but also removes the default admin notice, without the use of Javascript.

    here is some of the code

    / Validation filters

    $title = $album->post_title;
    if ( ! $title ) {
        $errors['title'] = "The title is required";
    }
    
    // if we have errors lets setup some messages
    if (! empty($errors)) {
    
        // we must remove this action or it will loop for ever
        remove_action('save_post', 'album_save_post');
    
        // save the errors as option
        update_option('album_errors', $errors);
    
        // Change post from published to draft
        $album->post_status = 'draft';
    
        // update the post
        wp_update_post( $album );
    
        // we must add back this action
        add_action('save_post', 'album_save_post');
    
        // admin_notice is create by a $_GET['message'] with a number that wordpress uses to
        // display the admin message so we will add a filter for replacing default admin message with a redirect
        add_filter( 'redirect_post_location', 'album_post_redirect_filter' );
    }
    

    You can see the full tutorial here

  3. When save_post runs, it has already saved the post on the database.

    If you are using ACF, it has built-in validation.

    However, if you need to validate things outside of ACF, such as post_title, things get a little more complicated.

    Looking into WordPress core code, more specifically at the wp-includes/post.php‘s update_post() function, there is no built-in way to intercept a request before it is saved on the database.

    However, we can hook pre_post_update and use header() and get_post_edit_link() to prevent the post from being saved.

    <?php
    
    /**
    *   Performs custom validation on custom post type "Site"
    */
    function custom_post_site_save($post_id, $post_data) {
        # If this is just a revision, don't do anything.
        if (wp_is_post_revision($post_id))
            return;
    
        if ($post_data['post_type'] == 'site') {
            # In this example, we will raise an error for post titles with less than 5 characters
            if (strlen($post_data['post_title']) < 5) {
                # Add a notification
                update_option('my_notifications', json_encode(array('error', 'Post title can't be less than 5 characters.')));
                # And redirect
                header('Location: '.get_edit_post_link($post_id, 'redirect'));
                exit;
            }
        }
    }
    add_action( 'pre_post_update', 'custom_post_site_save', 10, 2);
    
    /**
    *   Shows custom notifications on wordpress admin panel
    */
    function my_notification() {
        $notifications = get_option('my_notifications');
    
        if (!empty($notifications)) {
            $notifications = json_decode($notifications);
            #notifications[0] = (string) Type of notification: error, updated or update-nag
            #notifications[1] = (string) Message
            #notifications[2] = (boolean) is_dismissible?
            switch ($notifications[0]) {
                case 'error': # red
                case 'updated': # green
                case 'update-nag': # ?
                    $class = $notifications[0];
                    break;
                default:
                    # Defaults to error just in case
                    $class = 'error';
                    break;
            }
    
            $is_dismissable = '';
            if (isset($notifications[2]) && $notifications[2] == true)
                $is_dismissable = 'is_dismissable';
    
            echo '<div class="'.$class.' notice '.$is_dismissable.'">';
               echo '<p>'.$notifications[1].'</p>';
            echo '</div>';
    
            # Let's reset the notification
            update_option('my_notifications', false);
        }
    }
    add_action( 'admin_notices', 'my_notification' );
    
  4. I just want to add to the excellent answer of @Lucas Bustamante that the value of custom fields can be accessed via $_POST global.

    Thus, @Lucas Bustamante answer is also valid if the validation refers to a custom field. For example:

    <?php
    
    /**
    *   Performs custom validation on custom post type "Site"
    */
    function custom_post_site_save($post_id, $post_data) {
        # If this is just a revision, don't do anything.
        if (wp_is_post_revision($post_id))
            return;
    
        if ($post_data['post_type'] == 'site') {
            # In this example, we will raise an error for post titles with less than 5 characters
            if (strlen($_POST['zip_code']) < 5) {
                # Add a notification
                update_option('my_notifications', json_encode(array('error', 'Zip code can't be less than 5 characters.')));
                # And redirect
                header('Location: '.get_edit_post_link($post_id, 'redirect'));
                exit;
            }
        }
    }
    add_action( 'pre_post_update', 'custom_post_site_save', 10, 2);
    
    /**
    *   Shows custom notifications on wordpress admin panel
    */
    function my_notification() {
        $notifications = get_option('my_notifications');
    
        if (!empty($notifications)) {
            $notifications = json_decode($notifications);
            #notifications[0] = (string) Type of notification: error, updated or update-nag
            #notifications[1] = (string) Message
            #notifications[2] = (boolean) is_dismissible?
            switch ($notifications[0]) {
                case 'error': # red
                case 'updated': # green
                case 'update-nag': # ?
                    $class = $notifications[0];
                    break;
                default:
                    # Defaults to error just in case
                    $class = 'error';
                    break;
            }
    
            $is_dismissable = '';
            if (isset($notifications[2]) && $notifications[2] == true)
                $is_dismissable = 'is_dismissable';
    
            echo '<div class="'.$class.' notice '.$is_dismissable.'">';
               echo '<p>'.$notifications[1].'</p>';
            echo '</div>';
    
            # Let's reset the notification
            update_option('my_notifications', false);
        }
    }
    add_action( 'admin_notices', 'my_notification' );
    ``
    
  5. you can use “wp_insert_post_data” filter to modifier or check data before it is inserted into the database

    add_filter( 'wp_insert_post_data', 'validate_posttypes' );
    function wpb_custom_excerpt($data) {
        str_starts_with($data['post_title'],'wp')?$data['post_title']='wp'.$data['post_title']:null;
        return $data;
    }