I’m attempting to add a list table to a custom admin page. I’ve been following this guide and this reference implementation.

Unfortunately when I define $this->items to add the data, nothing displays in the admin page (not even the html around the list table) and there are no error messages. If I comment that line out (line 135), then it works except for the obvious lack of data.

My code:

<?php

add_action('admin_menu', 'add_example_menues');

function add_example_menues() {
    add_menu_page('test', 'test', 'administrator', 'test-top', 'build_test_page');
    add_submenu_page('test-top', 'All tests', 'All tests', 'administrator', 'test-top');
}

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

class test_List_Table extends WP_List_Table {
    function __construct() {
        parent::__construct( array(
            'singular' => 'test',
            'plural' => 'tests',
            'ajax' => false
        ));
    }


    function extra_tablenav ($which) {
        if ($which == "top") {
            echo "Top";
        }
        if ($which == "bottom") {
            echo "Bottom";
        }
    }

    function get_columns() {
        $columns = array(
            'id'    => 'ID',
            'title'     => 'Title',
            'user_id'   => 'User ID',
            'description'   => 'Description'
        );
        return $columns;
    }

    function get_sortable_columns() {
        return $sortable = array(
            'id'        => array('id',false),
            'title'         => array('title',false),
            'user_id'       => array('user_id',false)
        );
    }

    function prepare_items() {
        global $wpdb;

        //data normally gotten from non-wp database. WordPress db user has access, as per this SE post:
        //http://wordpress.stackexchange.com/questions/1604/using-wpdb-to-connect-to-a-separate-database
        $query = "SELECT `id`, `title`, `user_id`, `description` FROM otherdb.example_table";

        //pagination stuff
        $orderby = !empty($_GET["orderby"]) ? mysql_real_escape_string($_GET["orderby"]) : 'ASC';
        $order = !empty($_GET["order"]) ? mysql_real_escape_string($_GET["order"]) : '';
        if(!empty($orderby) & !empty($order)){ $query.=' ORDER BY '.$orderby.' '.$order; }
        $totalitems = $wpdb->query($query);
        echo "$totalitems";
        $per_page = 5;
        $paged = !empty($_GET["paged"]) ? mysql_real_escape_string($_GET["paged"]) : '';
        if(empty($paged) || !is_numeric($paged) || $paged<=0 ){ $paged=1; }
        $totalpages = ceil($totalitems/$perpage);
        if(!empty($paged) && !empty($perpage)){
            $offset=($paged-1)*$perpage;
            $query.=' LIMIT '.(int)$offset.','.(int)$perpage;
        }
        $this->set_pagination_args( array(
            "total_items" => 4,
            "total_pages" => 1,
            "per_page" => 5,
        ) );


        $columns = $this->get_columns();
        $hidden = array();
        $sortable = $this->get_sortable_columns();

        $this->_column_headers = array($columns, $hidden, $sortable);

        //actual data gotten from database, but for SE use hardcoded array
        //$data = $wpdb->get_results($query, ARRAY_N);

        $example_data = array(
                array(
                        'id'        => 1,
                        'title'     => 'nonsense',
                        'user_id'    => 1,
                        'description'  => 'asdf'
                ),
                array(
                        'id'        => 2,
                        'title'     => 'notanumber',
                        'user_id'    => 2,
                        'description'  => '404'
                ),
                array(
                        'id'        => 3,
                        'title'     => 'I Am A Title',
                        'user_id'    => 3,
                        'description'  => 'desc'
                ),
                array(
                        'id'        => 4,
                        'title'     => 'Example',
                        'user_id'    => 4,
                        'description'  => 'useless'
                ),
                array(
                        'id'        => 5,
                        'title'     => 'aeou',
                        'user_id'    => 5,
                        'description'  => 'keyboard layouts'
                ),
                array(
                        'id'        => 6,
                        'title'     => 'example data',
                        'user_id'    => 6,
                        'description'  => 'data example'
                ),
                array(
                        'id'        => 7,
                        'title'     => 'last one',
                        'user_id'       => 7,
                        'description'  => 'done'
                )
            );


        //This is the line:
        $this->items = $example_data;
        //When the above line is commented, it works as expected (except for the lack of data, of course)
    }
}


function build_test_page() {

    $testListTable = new test_List_Table();
    $testListTable->prepare_items();

?>
<div class="wrap">

    <div id="icon-users" class="icon32"><br/></div>
    <h2>List Table Test</h2>

    <?php $testListTable->display() ?>

</div>
<?php
}
?>

The above code is in a separate file, included into functions.php with a require_once().

I’m using wordpress 4.1.1

What is going on here? Why would everything disappear and no error be given?

2 s
2

I did same error first time I implemented WP_List_Table.

The problem is that when you call WP_List_Table::display() WordPress in turn calls:

  • WP_List_Table::display_rows_or_placeholder()
  • WP_List_Table::display_rows()
  • WP_List_Table::single_row()
  • WP_List_Table::single_row_columns()

Last function is called for every row. If you look at its code (see source), it has:

if ( 'cb' == $column_name ) {
   // you don't have 'cb' column, code remove because not relevant
} elseif ( method_exists( $this, 'column_' . $column_name ) ) {
   // you don't have method named 'column_{$column_name}',
   // code remove because not relevant 
} else {
   // not relevant line
   echo $this->column_default( $item, $column_name );
}

So WordPress calls WP_List_Table::column_default() for every row, but… that method doesn’t exist.

Add to your class:

public function column_default($item, $column_name) {
    return $item[$column_name];
}

And your data will be correctly displayed.

When you don’t add data, error doesn’t show up because with no data WordPress never calls display_rows() and other methods after that.


Bonus Notes:

  1. In your prepare_items() method you use the variable $perpage but you define it as $per_page (notice the underscore), that cause a division-by-zero warning (and pagination doesn’t work)

  2. the code mysql_real_escape_string($_GET["orderby"]) is not safe, because for 'orderby' SQL clause mysql_real_escape_string() is not enough. (see recent Yoast SQL injection bug).

    Do something like:

    $orderby = 'id'; // default
    $by = strtolower(filter_input(INPUT_GET, 'orderby', FILTER_SANITIZE_STRING));
    if (in_array($by, array('title', 'description', 'user_id'), true) {
        $orderby = $by;
    }
    

    and do something similar to for 'order' clause as well: very probably it can only be either ASC or DESC: don’t allow anything else.

Leave a Reply

Your email address will not be published. Required fields are marked *