Get started with 33% off your first certification using code: 33OFFNEW

How to create a CRUD in WordPress without using plugins

9 min read
Published on 23rd March 2023

We're going to dive into the world of WordPress and learn how to create a custom CRUD (Create, Read, Update, Delete) system in the WordPress admin area without relying on plugins. This tutorial will walk you through creating your own tables and managing data within them.

You can easily adapt this tutorial to create a CRUD system on the frontend rather than the admin.

This tutorial assumes you have a basic understanding of WordPress, PHP, and MySQL. It utilises some JavaScript and jQuery too - we aren't big fans of jQuery but it is available in the admin and WordPress depends on it. Let's get started!

Section 1: Creating a Custom Database Table

The first step in creating our custom CRUD system is to create a new table in the WordPress database. This table will hold the data for our custom CRUD system. We will be using the $wpdb global variable, which is an instance of the wpdb class, to interact with the WordPress database.

To create our new table, we'll add the following code to our theme's functions.php file:

function create_custom_table() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'custom_data';
    $charset_collate = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE $table_name (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        title varchar(255) NOT NULL,
        content text NOT NULL,
        created_at datetime NOT NULL,
        updated_at datetime NOT NULL,
        PRIMARY KEY  (id)
    ) $charset_collate;";

    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
}
after_switch_theme(__FILE__, 'create_custom_table');

This function will create a new table with columns for id, title, content, created_at, and updated_at. The after_switch_theme function is used to call our create_custom_table function when our theme is activated. This part is important, because if you're developing an already active theme your table won't be created - you'll need to deactive and reactivate it. If you've decided to build the CRUD as part of a plugin there are equivalent functions for when the plugin is activated. To keep things and focus on the CRUD code we've opted to build this into the theme for this tutorial, but in the real world it would be much more portable to build this as a plugin.

Here's some info on how after_switch_theme works.

For more information about creating tables, check the WordPress documentation on Creating Tables with Plugins.

Section 2: Creating a Custom Admin Menu Item

Now that we have our table set up, let's create a new menu item in the WordPress admin area to manage our custom data. We'll use the add_menu_page function to add a new menu item and the add_submenu_page function to create subpages for each CRUD operation.

Add the following code to your functions.php file:

function custom_data_menu() {
    $page_title = 'Custom Data';
    $menu_title = 'Custom Data';
    $capability = 'manage_options';
    $menu_slug = 'custom-data';
    $function = 'custom_data_page';
    $icon_url = 'dashicons-admin-generic';
    $position = 25;

    add_menu_page($page_title, $menu_title, $capability, $menu_slug, $function, $icon_url, $position);

    // Submenu pages
    add_submenu_page($menu_slug, 'Add New', 'Add New', $capability, 'custom-data-add', 'custom_data_add_page');
    add_submenu_page($menu_slug, 'Edit', 'Edit', $capability, 'custom-data-edit', 'custom_data_edit_page');
}
add_action('admin_menu', 'custom_data_menu');

function custom_data_page() {
    // Main custom data management page content
}

function custom_data_add_page() {
    // Add new custom data page content
}

function custom_data_edit_page() {
    // Edit custom data page content
}

This code will create a new menu item named "Custom Data" in the WordPress admin area. It also creates subpages for adding and editing custom data.

If you want to change the icon take a look at the available dashicons.

For more information about creating admin menu pages, check the WordPress documentation on Adding Administration Menus.

Section 3: Displaying Custom Data in the Admin Area

With the menu item and subpages in place, let's move on to displaying our custom data on the main "Custom Data" page. We'll query the custom table we created earlier and display the results in a table. To achieve this, add the following code to the custom_data_page function:

function custom_data_page() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'custom_data';
    $results = $wpdb->get_results("SELECT * FROM $table_name ORDER BY created_at DESC");

    echo '<div class="wrap">';
    echo '<h1 class="wp-heading-inline">Custom Data</h1>';
    echo '<a href="' . admin_url('admin.php?page=custom-data-add') . '" class="page-title-action">Add New</a>';
    echo '<hr class="wp-header-end">';

    echo '<table class="wp-list-table widefat fixed striped">';
    echo '<thead><tr><th>Title</th><th>Content</th><th>Created At</th><th>Updated At</th><th>Actions</th></tr></thead>';
    echo '<tbody>';

    foreach ($results as $row) {
        echo '<tr>';
        echo '<td>' . esc_html($row->title) . '</td>';
        echo '<td>' . esc_html($row->content) . '</td>';
        echo '<td>' . esc_html($row->created_at) . '</td>';
        echo '<td>' . esc_html($row->updated_at) . '</td>';
        echo '<td><a href="' . admin_url('admin.php?page=custom-data-edit&id=' . $row->id) . '">Edit</a> | <a href="#" class="delete-link" data-id="' . $row->id . '">Delete</a></td>';
        echo '</tr>';
    }
    echo '</tbody>';
    echo '</table>';
    echo '</div>';
}

This code queries the custom table, fetches all records, and displays them in a table format. The "Add New" button directs users to the "Add New" subpage, and each row has "Edit" and "Delete" links for managing individual records.

Section 4: Adding New Custom Data

Now that we can display our custom data, let's create a form to add new data. To do this, add the following code to the custom_data_add_page function:

function custom_data_add_page() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'custom_data';

    if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['custom_data_nonce']) && wp_verify_nonce($_POST['custom_data_nonce'], 'custom_data_add')) {
        $title = sanitize_text_field($_POST['title']);
        $content = sanitize_textarea_field($_POST['content']);
        $created_at = current_time('mysql');
        $updated_at = current_time('mysql');

        $wpdb->insert($table_name, compact('title', 'content', 'created_at', 'updated_at'));

        echo '<div class="notice notice-success is-dismissible"><p>Custom data added successfully!</p></div>';
        echo '<script>window.location.href="' . admin_url('admin.php?page=custom-data') . '";</script>';
    }

    echo '<div class="wrap">';
    echo '<h1 class="wp-heading-inline">Add New Custom Data</h1>';
    echo '<hr class="wp-header-end">';

    echo '<form method="post">';
    echo '<table class="form-table">';
    echo '<tr>';
    echo '<th scope="row"><label for="title">Title</label></th>';
    echo '<td><input name="title" type="text" id="title" class="regular-text" required></td>';
    echo '</tr>';
    echo '<tr>';
    echo '<th scope="row"><label for="content">Content</label></th>';
    echo '<td><textarea name="content" id="content" class="large-text" rows="10" required></textarea></td>';
    echo '</tr>';
    echo '</table>';

    echo '<input type="hidden" name="custom_data_nonce" value="' . wp_create_nonce('custom_data_add') . '">';
    echo '<p class="submit"><input type="submit" name="submit" id="submit" class="button button-primary" value="Add New"></p>';
    echo '</form>';
    echo '</div>';
}

This code creates a form for adding new custom data. When the form is submitted, it inserts the data into the custom table and redirects the user back to the main "Custom Data" page with a success message.

When inserting or editing any data in WordPress we always use the $wpdb->insert() method, which takes a number of attributes as an array to prevent SQL injections. We also sanitize the input using sanitize_text_field and sanitize_textarea_field (remember to always sanitize your input in WordPress). Check out our article on how to prevent SQL injections in all kinds of applications if you would like more information on this.

Section 5: Editing and Updating Custom Data

To edit existing custom data, we need to create a form similar to the one for adding new data. Add the following code to the custom_data_edit_page function:

function custom_data_edit_page() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'custom_data';
    $id = isset($_GET['id']) ? intval($_GET['id']) : 0;
    $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $id));

    if (!$row) {
        echo '<div class="notice notice-error is-dismissible"><p>Invalid custom data ID!</p></div>';
        return;
    }

    if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['custom_data_nonce']) && wp_verify_nonce($_POST['custom_data_nonce'], 'custom_data_edit')) {
        $title = sanitize_text_field($_POST['title']);
        $content = sanitize_textarea_field($_POST['content']);
        $updated_at = current_time('mysql');

        $wpdb->update($table_name, compact('title', 'content', 'updated_at'), array('id' => $id));

        echo '<div class="notice notice-success is-dismissible"><p>Custom data updated successfully!</p></div>';
        echo '<script>window.location.href="' . admin_url('admin.php?page=custom-data') . '";</script>';
    }

    echo '<div class="wrap">';
    echo '<h1 class="wp-heading-inline">Edit Custom Data</h1>';
    echo '<hr class="wp-header-end">';

    echo '<form method="post">';
    echo '<table class="form-table">';
    echo '<tr>';
    echo '<th scope="row"><label for="title">Title</label></th>';
    echo '<td><input name="title" type="text" id="title" value="' . esc_attr($row->title) . '" class="regular-text" required></td>';
    echo '</tr>';
    echo '<tr>';
    echo '<th scope="row"><label for="content">Content</label></th>';
    echo '<td><textarea name="content" id="content" class="large-text" rows="10" required>' . esc_textarea($row->content) . '</textarea></td>';
    echo '</tr>';
    echo '</table>';

    echo '<input type="hidden" name="custom_data_nonce" value="' . wp_create_nonce('custom_data_edit') . '">';
    echo '<p class="submit"><input type="submit" name="submit" id="submit" class="button button-primary" value="Update"></p>';
    echo '</form>';
    echo '</div>';
}

This code creates a form for editing existing custom data. When the form is submitted, it updates the data in the custom table and redirects the user back to the main "Custom Data" page with a success message.

Section 6: Deleting Custom Data

The final CRUD operation we need to implement is deleting custom data. We'll use AJAX to handle the deletion process. Add the following code to your functions.php file:

function delete_custom_data() {
    check_ajax_referer('delete_custom_data_nonce', 'nonce');
    global $wpdb;
    $table_name = $wpdb->prefix . 'custom_data';
    $id = isset($_POST['id']) ? intval($_POST['id']) : 0;

    $result = $wpdb->delete($table_name, array('id' => $id));
    wp_send_json_success($result);
}
add_action('wp_ajax_delete_custom_data', 'delete_custom_data');

function custom_data_admin_scripts() {
    wp_enqueue_script('custom-data', get_template_directory_uri() . '/js/custom-data.js', array('jquery'), false, true);
    wp_localize_script('custom-data', 'customData', array(
        'ajaxurl' => admin_url('admin-ajax.php'),
        'delete_nonce' => wp_create_nonce('delete_custom_data_nonce')
    ));
}
add_action('admin_enqueue_scripts', 'custom_data_admin_scripts');

This code registers the AJAX action for deleting custom data and enqueues a JavaScript file named custom-data.js. We'll create this file in the next step.

Next, create a new file named custom-data.js in your theme's js directory and add the following code:

jQuery(document).ready(function ($) {
    $('.delete-link').on('click', function (e) {
        e.preventDefault();
        if (!confirm('Are you sure you want to delete this custom data?')) {
            return;
        }

        var id = $(this).data('id');
        $.post(customData.ajaxurl, {
            action: 'delete_custom_data',
            nonce: customData.delete_nonce,
            id: id
        }, function (response) {
            if (response.success) {
                location.reload();
            } else {
                alert('An error occurred while deleting the custom data.');
            }
        });
    });
});

This JavaScript code listens for clicks on the "Delete" links in the custom data table. When a link is clicked, it sends an AJAX request to delete the corresponding custom data record and refreshes the page upon successful deletion.

That's it! You now have a fully functional custom CRUD system in the WordPress admin area without using any plugins. This tutorial has covered creating custom database tables, managing data with CRUD operations, and creating custom admin pages. You can now extend and modify this code to fit your specific needs.

For further reading, please refer to the WordPress Plugin Handbook.

There's a number of things you could do to improve this system, why not give it a go and add some further features:

  • Add the ability to record the author against each entry
  • Use the WordPress WYSIWYG editor instead of a text area
  • Add form validation to prevent empty fields being submitted, and ensure the title is at least 10 characters long