1

I'm trying to develop a simple plugin but I'm having a problem: I have a custom WP_List_Table in an admin page, but when I click on a button like "Edit Row" or "Delete Row" it performs some expected action and try to refresh the page using wp_redirect(). This is causing a problem:

Warning: Cannot modify header information - headers already sent by (output started at /opt/lampp/htdocs/wordpress/wp-includes/formatting.php:5652) in /opt/lampp/htdocs/wordpress/wp-includes/pluggable.php on line 1265

Warning: Cannot modify header information - headers already sent by (output started at /opt/lampp/htdocs/wordpress/wp-includes/formatting.php:5652) in /opt/lampp/htdocs/wordpress/wp-includes/pluggable.php on line 1268

I have just the Advanced Custom Fields plugin installed and I'm using the default theme. At the moment, my plugin is JUST this code below (I got it here https://www.sitepoint.com/using-wp_list_table-to-create-wordpress-admin-tables/):

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

class Customers_List extends WP_List_Table {

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

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

    }


    /**
    * Retrieve customers data from the database
    *
    * @param int $per_page
    * @param int $page_number
    *
    * @return mixed
    */
    public static function get_customers() {

        $result = array();

        $args = array(
            'role'    => 'Inscrito',
        );

        $users = get_users( $args );

        foreach($users as $user){

            $user_form = 'user_'. $user->ID;

            $image = get_field('receipt', $user_form);
            $size = 'full'; // (thumbnail, medium, large, full or custom size)

            if( $image ) {
                $image_url = '<a href="' .$image['url'] . '"> a </a>';
            }else{
                $image_url = 'False';
            }

            array_push($result, 
                        array(  'ID' => $user->ID,
                                'name' => $user->status,
                                'address' => $image_url, 
                                'city' => $image_url, 
                        )
            );
        }

        return $result;
    }


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

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

    /** Text displayed when no customer data is available */
    public function no_items() {
        _e( 'No customers avaliable.', 'sp' );
    }


    /**
    * 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 'address':
            case 'city':
                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( 'sp_delete_customer' );

        $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" />',
            'name'    => __( 'Name', 'sp' ),
            'address' => __( 'Address', 'sp' ),
            'city'    => __( 'City', 'sp' )
        ];

        return $columns;
    }


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

        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();

        $users = self::get_customers();

        $per_page     = $this->get_items_per_page( 'customers_per_page', 5 );
        $current_page = $this->get_pagenum();
        $total_items  = self::record_count();
        $slice_offset = ( ( $current_page-1 )* $per_page );
        $slice_limit = $slice_offset+$per_page;

        // only ncessary because we have sample data
        $found_data = array_slice( $users, $slice_offset, $slice_limit);

        $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 = $found_data;
    }

    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, 'sp_delete_customer' ) ) {
                die( 'Go get a life script kiddies' );
            }
            else {
                self::delete_customer( absint( $_GET['customer'] ) );

                        // 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_customer( $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 SP_Plugin {

    // class instance
    static $instance;

    // customer WP_List_Table object
    public $customers_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(
            'Sitepoint WP_List_Table Example',
            'SP WP_List_Table',
            'manage_options',
            'wp_list_table_class',
            [ $this, 'plugin_settings_page' ]
        );

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

    }


    /**
    * Plugin settings page
    */
    public function plugin_settings_page() {
        ?>
        <div class="wrap">
            <h2>WP_List_Table Class Example</h2>

            <div id="poststuff">
                <div id="post-body" class="metabox-holder columns-2">
                    <div id="post-body-content">
                        <div class="meta-box-sortables ui-sortable">
                            <form method="post">
                                <?php
                                $this->customers_obj->prepare_items();
                                $this->customers_obj->display(); ?>
                            </form>
                        </div>
                    </div>
                </div>
                <br class="clear">
            </div>
        </div>
    <?php
    }

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

        $option = 'per_page';
        $args   = [
            'label'   => 'Customers',
            'default' => 5,
            'option'  => 'customers_per_page'
        ];

        add_screen_option( $option, $args );

        $this->customers_obj = new Customers_List();
    }


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

        return self::$instance;
    }

}

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

I know this is a frequently asked question here, but I tried to search a lot before asking and nothing helped me.

1

I'm not 100% certain this is your problem, but... I believe your "no_items" function should return the value, not echo it. If there are no items, that would lead to a "headers already sent" when the redirect is fired. Try it like this:

/** Text displayed when no customer data is available */
public function no_items() {
    return __( 'No customers avaliable.', 'sp' );
}
|improve this answer|||||
  • Thank you for your answer, but unfortunately it does not work for me – Julio Cesar de Azeredo Dec 8 '19 at 16:37
0

This code uses another method to show a "sucess" or "deleted" message without using wp_redirect: https://github.com/pmbaldha/WP-Custom-List-Table-With-Database-Example

|improve this answer|||||

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.