Add search bar to Custom Database Table in WordPress Dashboard

I have built a custom table within the WordPress database. From there I have put together a plugin which creates a dashboard page allowing you to view the various rows within the database by extending the WP_List_Table function. What I need to do now is add a search bar functionality. I have been able to add the search bar and loads the results page, however with no results given.

I need to be able to search a specific column within the custom database titled ‘Card’ and display the results.

Here is what I have so far. Any direction and case specific samples would be gratefully appreciated.

 <?php

 if ( ! class_exists( 'WP_List_Table' ) ) {
    require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
 }

 class Members_List extends WP_List_Table {

    /** Class constructor */
    public function __construct() {

        parent::__construct( [
            'singular' => __( 'Member', 'red' ), //singular name of the listed records
            'plural'   => __( 'Members', 'red' ), //plural name of the listed records
            'ajax'     => false //does this table support ajax?
        ] );

    }


    /**
     * Retrieve members data from the database
     *
     * @param int $per_page
     * @param int $page_number
     *
     * @return mixed
     */
    public static function get_members( $per_page = 5, $page_number = 1 ) {

        global $wpdb;

        $sql = "SELECT * FROM  my_members";

        if ( ! empty( $_REQUEST['orderby'] ) ) {
            $sql .= ' ORDER BY ' . esc_sql( $_REQUEST['orderby'] );
            $sql .= ! empty( $_REQUEST['order'] ) ? ' ' . esc_sql( $_REQUEST['order'] ) : ' ASC';
        }

        $sql .= " LIMIT $per_page";
        $sql .= ' OFFSET ' . ( $page_number - 1 ) * $per_page;

        if( ! empty( $_REQUEST['s'] ) ){
        $search = esc_sql( $_REQUEST['s'] );
        $sql .= " WHERE card LIKE '%{$search}%'";
    }

        $result = $wpdb->get_results( $sql, 'ARRAY_A' );

        return $result;
    }


    /**
     * Delete a members record.
     *
     * @param int $id member ID
     */
    public static function delete_member( $id ) {
        global $wpdb;

        $wpdb->delete(
            "my_members",
            [ 'ID' => $id ],
            [ '%d' ]
        );
    }

    /**
     * Returns the count of records in the database.
     *
     * @return null|string
     */
    public static function record_count() {
        global $wpdb;

        $sql = "SELECT COUNT(*) FROM my_members";

        return $wpdb->get_var( $sql );
      }


    /** Text displayed when no member data is available */
    public function no_items() {
        _e( 'The Members Database is currently empty.', 'red' );
    }


/**
 * Render a column when no column specific method exist.
 *
 * @param array $item
 * @param string $column_name
 *
 * @return mixed
 */
public function column_default( $item, $column_name ) {
    switch ( $column_name ) {
        case 'Member_ID':
        case 'Card':
        case 'First':
        case 'Last':
        case 'Class':
            return $item[ $column_name ];
        default:
            return print_r( $item, true ); //Show the whole array for troubleshooting purposes
    }
}

/**
 * Render the bulk edit checkbox
 *
 * @param array $item
 *
 * @return string
 */
function column_cb( $item ) {
    return sprintf(
        '<input type="checkbox" name="bulk-delete[]" value="%s" />', $item['ID']
    );
}


/**
 * Method for name column
 *
 * @param array $item an array of DB data
 *
 * @return string
 */
function column_name( $item ) {

    $delete_nonce = wp_create_nonce( 'red_delete_member' );

    $title = '<strong>' . $item['name'] . '</strong>';

    $actions = [
        'delete' => sprintf( '<a href="?page=%s&action=%s&customer=%s&_wpnonce=%s">Delete</a>', esc_attr( $_REQUEST['page'] ), 'delete', absint( $item['ID'] ), $delete_nonce )
    ];

    return $title . $this->row_actions( $actions );
}


/**
 *  Associative array of columns
 *
 * @return array
 */
function get_columns() {
    $columns = [
        'cb'      => '<input type="checkbox" />',
        'Member_ID' => __( 'Member ID', 'red' ),
        'Card'    => __( 'Card', 'red' ),
        'First' => __( 'First Name', 'red' ),
        'Last'    => __( 'Last Name', 'red' )
    ];

    return $columns;
}


/**
 * Columns to make sortable.
 *
 * @return array
 */
public function get_sortable_columns() {
    $sortable_columns = array(
        'Member_ID' => array( 'Member_ID', true ),
        'Card' => array ( 'Card', true)
    );

    return $sortable_columns;
}

/**
 * Returns an associative array containing the bulk action
 *
 * @return array
 */
public function get_bulk_actions() {
    $actions = [
        'bulk-delete' => 'Delete'
    ];

    return $actions;
}


/**
 * Handles data query and filter, sorting, and pagination.
 */
public function prepare_items() {

    $this->_column_headers = $this->get_column_info();

    /** Process bulk action */
    $this->process_bulk_action();

    $per_page     = $this->get_items_per_page( 'members_per_page', 7 );
    $current_page = $this->get_pagenum();
    $total_items  = self::record_count();

    $this->set_pagination_args( [
        'total_items' => $total_items, //WE have to calculate the total number of items
        'per_page'    => $per_page //WE have to determine how many items to show on a page
    ] );

    $this->items = self::get_members( $per_page, $current_page );
}

public function process_bulk_action() {

    //Detect when a bulk action is being triggered...
    if ( 'delete' === $this->current_action() ) {

        // In our file that handles the request, verify the nonce.
        $nonce = esc_attr( $_REQUEST['_wpnonce'] );

        if ( ! wp_verify_nonce( $nonce, 'red_delete_member' ) ) {
            die( '' );
        }
        else {
            self::delete_member( absint( $_GET['member'] ) );

                    // esc_url_raw() is used to prevent converting ampersand in url to "#038;"
                    // add_query_arg() return the current url
                    wp_redirect( esc_url_raw(add_query_arg()) );
            exit;
        }

    }

    // If the delete bulk action is triggered
    if ( ( isset( $_POST['action'] ) && $_POST['action'] == 'bulk-delete' )
         || ( isset( $_POST['action2'] ) && $_POST['action2'] == 'bulk-delete' )
    ) {

        $delete_ids = esc_sql( $_POST['bulk-delete'] );

        // loop over the array of record IDs and delete them
        foreach ( $delete_ids as $id ) {
            self::delete_member( $id );

        }

        // esc_url_raw() is used to prevent converting ampersand in url to "#038;"
            // add_query_arg() return the current url
            wp_redirect( esc_url_raw(add_query_arg()) );
        exit;
    }
    }

 }

 class Member_Management_Plugin {

    // class instance
    static $instance;

    // member WP_List_Table object
    public $members_obj;

    // class constructor
    public function __construct() {
    add_filter( 'set-screen-option', [ __CLASS__, 'set_screen' ], 10, 3 );
    add_action( 'admin_menu', [ $this, 'plugin_menu' ] );
}


public static function set_screen( $status, $option, $value ) {
    return $value;
}

public function plugin_menu() {

    $hook = add_menu_page(
        'Manage Members',
        'Manage Members',
        'manage_options',
        'wp_list_table_class',
        [ $this, 'plugin_settings_page' ]
    );

    add_action( "load-$hook", [ $this, 'screen_option' ] );

}


/**
 * Plugin page
 */
public function plugin_settings_page() {
    ?>
    <style>
      table {display: block;overflow-x: scroll;max-width:98%;}
      th {min-width:100px;font-size:10px;}
    </style>
        <h2>IBEW 353 Member Management Portal</h2>
                <div id="post-body-content">
                    <div class="meta-box-sortables ui-sortable">
                        <form method="post">
   <input type="hidden" name="page" value="wp_list_table_class" />
                            <?php
                            $this->members_obj->prepare_items();
                            $this->members_obj->search_box('Search', 'search');
                            $this->members_obj->display(); ?>
                        </form>
                    </div>
                </div>
            <br class="clear">
<?php
}

/**
 * Screen options
 */
public function screen_option() {

    $option = 'per_page';
    $args   = [
        'label'   => 'Members',
        'default' => 7,
        'option'  => 'members_per_page'
    ];

    add_screen_option( $option, $args );

    $this->members_obj = new Members_List();
}


/** Singleton instance */
public static function get_instance() {
    if ( ! isset( self::$instance ) ) {
        self::$instance = new self();
    }

    return self::$instance;
    }

 }


 add_action( 'plugins_loaded', function () {
    Member_Management_Plugin::get_instance();
 } );

UPDATE:
As you see I have the search bar visible now but am not able to correctly fetch the results. What would I add in order to search the Card column and display the results and how would I add it.

How would I add the search form in order to load the page with the new results from the search bar? Any code samples would be gratefully appreciated.

I have tried the suggestions provided and reviewed the reference link however the links tutorial appears to ahve broken code within the content and as such was hesitant I am already trying to debug one solution.

With hands in the air I am relying on the community for further insight.

Solutions Collecting From Web of "Add search bar to Custom Database Table in WordPress Dashboard"

Just add it in where you’re doing your custom query called by prepare items:

public static function get_members( $per_page = 5, $page_number = 1 ) {

    global $wpdb;

    $sql = 'SELECT * FROM  my_members';

    if ( ! empty( $_REQUEST['orderby'] ) ) {
        $sql .= ' ORDER BY ' . esc_sql( $_REQUEST['orderby'] );
        $sql .= ! empty( $_REQUEST['order'] ) ? ' ' . esc_sql( $_REQUEST['order'] ) : ' ASC';
    }

    $sql .= " LIMIT $per_page";
    $sql .= ' OFFSET ' . ( $page_number - 1 ) * $per_page;

    if( ! empty( $_REQUEST['s'] ) ){
        $search = esc_sql( $_REQUEST['s'] );
        $sql .= " WHERE card LIKE '%{$search}%'";
    }

    $result = $wpdb->get_results( $sql, 'ARRAY_A' );

    return $result;
}

I haven’t tested this, but that should be what you’re looking for. You should also look at using $wpdb and included functions like $wpdb->prepare to formulate your SQL queries using WordPress built in handling, escaping, etc

https://developer.wordpress.org/reference/classes/wpdb/prepare/

UPDATE:
You also need to update anywhere you make custom database queries to handle including the WHERE statement, including counts, etc.

I recommend that you review this example, and rewrite your entire list table class based on this tutorial, which uses a custom database table data, and has the search handling integrated as well:
https://webkul.com/blog/create-admin-tables-using-wp_list_table-class/