When developing custom WordPress functionality, displaying structured data in the administration area often becomes necessary. While WordPress uses WP_List_Table internally to display posts, pages, and users, developers can also extend this class to build custom admin screens with pagination, search, and action handlers.
In this tutorial, we will walk through creating a custom admin page that manages user approval processes using WP_List_Table and WP_User_Query.
1. Registering the Custom Admin Menu
First, we need to register a menu item in the WordPress admin sidebar. We will also include a dynamic badge showing the count of pending accounts waiting for approval.
if (!defined('ABSPATH')) {
exit;
}
if (!class_exists('WP_List_Table')) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
add_action('admin_menu', 'wg_register_approve_researchers_menu');
function wg_register_approve_researchers_menu() {
$count = count(get_users([
'role' => 'researcher',
'meta_key' => 'status_account',
'meta_value' => 'Reviewing',
'fields' => 'ID'
]));
$title = 'Approve Researchers';
if ($count) {
$title .= ' ' . $count . '';
}
add_menu_page(
'Approve Researchers',
$title,
'list_users',
'approve-researchers',
'wg_approve_researchers_page',
'dashicons-groups',
25
);
}
Key points in this setup:
- class-wp-list-table.php: Since WP_List_Table is an internal core class, it must be explicitly loaded if not already available.
- Dynamic Badge: Querying pending users allows us to append to the menu title for native notification styling
2. Creating the Custom List Table Class
To display user records cleanly, we extend WP_List_Table. We define custom columns, sortable fields, and the prepare_items() method to query users with pagination and search functionality.
class WG_Approve_Researchers_Table extends WP_List_Table {
public function get_columns() {
return [
'display_name' => 'Name',
'user_email' => 'Email',
'status' => 'Status'
];
}
public function get_sortable_columns() {
return [
'display_name' => ['display_name', true],
'user_email' => ['user_email', false]
];
}
public function prepare_items() {
$per_page = 20;
$current_page = $this->get_pagenum();
$search = !empty($_REQUEST['s']) ? sanitize_text_field($_REQUEST['s']) : '';
$args = [
'role' => 'researcher',
'number' => $per_page,
'paged' => $current_page,
'search' => $search ? '*' . $search . '*' : '',
'search_columns' => ['user_login', 'user_email', 'display_name'],
'meta_key' => 'status_account',
'meta_value' => 'Reviewing'
];
if (!empty($_GET['orderby'])) {
$args['orderby'] = sanitize_key($_GET['orderby']);
}
if (!empty($_GET['order'])) {
$args['order'] = sanitize_key($_GET['order']);
}
$query = new WP_User_Query($args);
$this->items = $query->get_results();
$this->set_pagination_args([
'total_items' => $query->get_total(),
'per_page' => $per_page,
'total_pages' => ceil($query->get_total() / $per_page)
]);
$this->_column_headers = [
$this->get_columns(),
[],
$this->get_sortable_columns()
];
}
public function column_display_name($item) {
$url = admin_url('admin.php?page=approve-researchers&user_id=' . $item->ID);
$actions = [
'view' => 'View'
];
return sprintf(
'%s%s',
esc_url($url),
esc_html($item->display_name),
$this->row_actions($actions)
);
}
public function column_user_email($item) {
return esc_html($item->user_email);
}
public function column_status($item) {
return esc_html(get_user_meta($item->ID, 'status_account', true));
}
public function column_default($item, $column_name) {
switch ($column_name) {
case 'user_email':
return $this->column_user_email($item);
case 'status':
return $this->column_status($item);
}
return '';
}
}
3. Handling User Details and Approval Actions
Finally, we implement the page callback function wg_approve_researchers_page(). It processes the approval POST request, sends a transactional notification email with login credentials, and renders either the table view or individual record details.
function wg_approve_researchers_page() {
if (isset($_POST['approve_user'])) {
check_admin_referer('wg_approve_user');
$user_id = absint($_POST['user_id']);
$paymentLink = $_POST['payment_link'];
if ($user_id) {
update_user_meta($user_id, 'status_account', 'Active');
update_user_meta($user_id, 'payment_statust', 'Unpaid');
$user_data = get_userdata($user_id);
if ($user_data) {
$new_password = wp_generate_password(12, true, false);
wp_set_password($new_password, $user_id);
$site_name = get_bloginfo('name');
$site_url = home_url();
$email = $user_data->user_email;
$full_name = $user_data->display_name;
$to = $email;
$subject = 'Account Approved - ' . $site_name;
$body = "
Hi " . esc_html($full_name) . ",
Great news! Your account on " . esc_html($site_name) . " has been approved and successfully activated by our team.
Next, you'll need to pay for access to our service. Once we've verified your payment, you'll receive your account login credentials.
Your Payment Details:
Link: Pay Here
Regards,
All at " . esc_html($site_name) . "
";
$headers = array('Content-Type: text/html; charset=UTF-8');
wp_mail($to, $subject, $body, $headers);
}
}
wp_safe_redirect(admin_url('admin.php?page=approve-researchers&user_id=' . $user_id));
exit;
}
$user = false;
if (!empty($_GET['user_id'])) {
$user = get_user_by('ID', absint($_GET['user_id']));
}
if (!$user) {
$table = new WG_Approve_Researchers_Table();
$table->prepare_items();
?>
Approve Researchers
ID, 'status_account', true);
?>
Conclusion & CTA
Extending WP_List_Table gives your custom WordPress administration features a clean, native UI while supporting standard workflows like sorting, pagination, and searching out of the box.
Be sure to check out our previous tutorials on WordPress development:
Building Custom Gutenberg Blocks Without Webpack: A Practical Dynamic Block Example
jQuery.ajax vs Vanilla JavaScript: Fetch and XHR Compared
If you have any questions, feel free to contact us. We’re here to help! Drop us a message with your project details, and our team will get back to you as soon as possible.