How to validate custom fields for a custom post type before insert?

I hacked the sh*t out of it. Here is my uggliest solution ever to a simple problem. I am not really happy with this, but it will have to do.

For the sake of this example, I will use a custom post type called book and we’re gonna validate a field called ISBN. This solution uses PHP Session and header redirect.

Here we go…

Step 1 – Data Validation

The data validation happens using the filter hook wp_insert_post_data, right before the data is inserted in the database. If it is invalid, we set an invalid flag and redirect the user before the data is inserted into the database.

function pre_insert_book($data, $postarr)
{
    if ($data['post_type'] === "book" && in_array($data['post_status'], ['publish', 'draft', 'pending']) && !empty($_POST) && isset($_POST['ISBN'])) {
        $ISBN = sanitize_text_field($_POST['ISBN']);
        if (empty($ISBN)) {
            session_start();
            $_SESSION['POST'] = $_POST;
            header("Location: " . admin_url('post-new.php?post_type=book&invalid=empty_ISBN'));
            exit;
        } elseif (!validade_ISBN($ISBN)) {
            session_start();
            $_SESSION['POST'] = $_POST;
            header("Location: " . admin_url('post-new.php?post_type=book&invalid=invalid_ISBN'));
            exit;
        }
    }
    return $data;
}
add_filter('wp_insert_post_data', 'pre_insert_book', 10, 2);

Step 2 – Display error notice

If an invalid flag is thrown, we display an error to the user. We also preserve the book title so the data isn’t lost on form submission. I’m sure this could be done elsewhere.

function check_insert_book_error_notices()
{
    global $current_screen, $post;

    if ($current_screen->parent_file === "edit.php?post_type=book" && isset($_GET['invalid'])) {

        session_start();

        // We want to keep the book title like so (and other values if necessary)
        if (isset($_SESSION["POST"]) && isset($_SESSION["POST"]['post_title']))
            $post->post_title = $_SESSION["POST"]['post_title'];

        if (esc_attr($_GET['invalid']) === "empty_ISBN") {
            echo '<div class="error"><p>ISBN number cant be empty</p></div>';
        } elseif (esc_attr($_GET['invalid']) === "invalid_ISBN") {
            echo '<div class="error"><p>ISBN is not valid</p></div>';
        }
    }
}
add_action('admin_notices', 'check_insert_book_error_notices');

Step 3 – Dealing with the ISBN form field and Session

It’s not fun to return to an empty form when a error occurs, nor it is ever a good practice to leave a session open. So we deal with this here.

function load_custom_book_meta_boxes()
{
    add_meta_box('isbn-form', 'Book data', array($this, 'custom_form_for_book_post_type'), 'book', 'normal', 'high');
}
add_action('add_meta_boxes', 'load_custom_book_meta_boxes');

function custom_form_for_book_post_type($post)
{

    session_start();

    $ISBN_previous_value = "";
    if (isset($_SESSION["POST"]) && isset($_SESSION["POST"]['ISBN']))
        $ISBN_previous_value = sanitize_text_field($_SESSION["POST"]['ISBN']);

    session_destroy();

    ob_start(); ?>

    <div id="isbn-form-container">
        <form class="isbn-form" method="post" action="">
            <table>
                <tr class="isbn-row">
                    <td><label for="ISBN">Book ISBN</label></td>
                    <td><input type="number" id="ISBN" class="required" name="ISBN" value="<?php echo $ISBN_previous_value; ?>" autocomplete="off" required></td>
                </tr>
            </table>
        </form>
    </div>

<?php ob_end_flush();
}

Everything seems to be working fine; I only did a bit of testing and will return later to do some more.

Hope it helps some folks wanting something similar.