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.
2 Answers
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' );
}