5

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?

  • They have written in codex, that the class could change and dev's should have copied this class for non-problematic table render XD> – Nikita Jul 1 '16 at 8:52
9
+50

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:

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.

  • Damn, you beat me by seconds.. :) – Nicolai Mar 31 '15 at 23:02
  • @ialocin :) Well, I knew the answer just 1 sec after reading the question, but I spent 10+ minutes in writing to add all the formatting and the links in the right place. – gmazzap Mar 31 '15 at 23:05
  • Haha, yeah me too, I mean I spend some time on rewriting. Of course I had to look it up, otherwise I would have known 4 hours ago.. – Nicolai Mar 31 '15 at 23:07
  • I did this error 1st time I wrote a WP_List_Table, so I recognized it immediately :) – gmazzap Mar 31 '15 at 23:09
  • Now I will remember too :) But there is THE difference I might be capable, but you have much more actual knowledge - besides being much more capable too of course :) Anyway its answered and solved! I'm happy :) – Nicolai Mar 31 '15 at 23:14
2

Now at home, I actually ran your code. I got this:

Fatal error: Call to undefined function convert_to_screen() in .../wp-admin/includes/class-wp-list-table.php on line 88

Note:

Please turn on Debugging_in_WordPress when developing.

Anyway this lead me to the following question on Stackoverflow:

@brasofilo's answer helped a lot, not surprising as he is one of the leading people on WordPress Development too.

Anyway following his answer, you are missing the method column_default, additionally the _construct method is different. But you can read that up yourself, here is some working - stripped down - code:

add_action( 'admin_menu', 'add_test_list_table_menues' );
function add_test_list_table_menues() {
    add_menu_page(
        'test', 
        'test', 
        'manage_options', 
        'test-top', 
        'test_list_table_output'
    );
}

function test_list_table_output() { 
    echo '<div class="wrap">';
    echo '<h2>Test List Table</h2>';
    new Test_List_Table(); 
    echo '</div>';
}

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 {
    public function __construct() {
        parent::__construct( array(
            'singular' => 'test',
            'plural' => 'tests',
            'ajax' => false
        ));
        $this->prepare_items();
        $this->display();
    }

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

    function column_default( $item, $column_name ) {
        switch( $column_name ) {
            case 'tid':
            case 'title':
            case 'user_id':
            case 'description':
                return $item[ $column_name ];
            default:
                return print_r( $item, true ) ;
        }
    }

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

        $columns = $this->get_columns();
        $hidden = array();
        $sortable = $this->get_sortable_columns();
        $this->_column_headers = array($columns, $hidden, $sortable);
        $this->items = $example_data;
    }
}
  • Too bad, probably too obvious.. @gandalf3 – Nicolai Mar 31 '15 at 19:20
  • Thanks for the debugging tip. I'd accept both if I could! – gandalf3 Apr 1 '15 at 18:31
  • No worries! @gandalf3 – Nicolai Apr 1 '15 at 21:23
  • If you run this code on the latest version of WP (4.4.2), it still breaks and shows the "Call to undefined function convert_to_screen()" error FYI – Andy Lobel Feb 16 '16 at 12:25
  • Well, the only thing I can say at the moment is, I'm pretty sure it worked back then. Thanks for notifying me though, I try to come back to this soon. @AndyLobel – Nicolai Feb 16 '16 at 14:34

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.