: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* Get a list of all, hidden and sortable columns, with filter applied
protected function get_column_info() {
// $_column_headers is already set / cached
if ( isset( $this->_column_headers ) && is_array( $this->_column_headers ) ) {
// Back-compat for list tables that have been manually setting $_column_headers for horse reasons.
// In 4.3, we added a fourth argument for primary column.
$column_headers = [ [], [], [], $this->get_primary_column_name() ];
foreach ( $this->_column_headers as $key => $value ) {
$column_headers[ $key ] = $value;
$columns = get_column_headers( $this->screen );
$hidden = get_hidden_columns( $this->screen );
$sortable_columns = $this->get_sortable_columns();
* Filters the list table sortable columns for a specific screen.
* The dynamic portion of the hook name, `$this->screen->id`, refers
* to the ID of the current screen, usually a string.
* @param array $sortable_columns An array of sortable columns.
$_sortable = apply_filters( "manage_{$this->screen->id}_sortable_columns", $sortable_columns );
foreach ( $_sortable as $id => $data ) {
if ( ! isset( $data[1] ) ) {
$sortable[ $id ] = $data;
$primary = $this->get_primary_column_name();
$this->_column_headers = [ $columns, $hidden, $sortable, $primary ];
return $this->_column_headers;
* Return number of visible columns
public function get_column_count() {
list ( $columns, $hidden ) = $this->get_column_info();
$hidden = array_intersect( array_keys( $columns ), array_filter( $hidden ) );
return count( $columns ) - count( $hidden );
* Print column headers, accounting for hidden and sortable columns.
* @staticvar int $cb_counter
* @param bool $with_id Whether to set the id attribute or not
public function print_column_headers( $with_id = true ) {
list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
$current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
$current_url = remove_query_arg( 'paged', $current_url );
if ( isset( $_GET['orderby'] ) ) {
$current_orderby = $_GET['orderby'];
if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) {
if ( ! empty( $columns['cb'] ) ) {
$columns['cb'] = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . __( 'Select All' ) . '</label>'
. '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />';
foreach ( $columns as $column_key => $column_display_name ) {
$class = [ 'manage-column', "column-$column_key" ];
if ( in_array( $column_key, $hidden ) ) {
if ( 'cb' === $column_key ) {
$class[] = 'check-column';
} elseif ( in_array( $column_key, [ 'posts', 'comments', 'links' ] ) ) {
if ( $column_key === $primary ) {
$class[] = 'column-primary';
if ( isset( $sortable[ $column_key ] ) ) {
list( $orderby, $desc_first ) = $sortable[ $column_key ];
if ( $current_orderby === $orderby ) {
$order = 'asc' === $current_order ? 'desc' : 'asc';
$class[] = $current_order;
$order = $desc_first ? 'desc' : 'asc';
$class[] = $desc_first ? 'asc' : 'desc';
$column_display_name = '<a href="' . esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ) . '"><span>' . $column_display_name . '</span><span class="sorting-indicator"></span></a>';
$tag = ( 'cb' === $column_key ) ? 'td' : 'th';
$scope = ( 'th' === $tag ) ? 'scope="col"' : '';
$id = $with_id ? "id='$column_key'" : '';
if ( ! empty( $class ) ) {
$class = "class='" . join( ' ', $class ) . "'";
echo "<$tag $scope $id $class>$column_display_name</$tag>";
public function display() {
$singular = $this->_args['singular'];
$this->display_tablenav( 'top' );
$this->screen->render_screen_reader_content( 'heading_list' );
<table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>">
<?php $this->print_column_headers(); ?>
echo " data-wp-lists='list:$singular'";
<?php $this->display_rows_or_placeholder(); ?>
<?php $this->print_column_headers( false ); ?>
$this->display_tablenav( 'bottom' );
* Get a list of CSS classes for the PUM_ListTable table tag.
* @return array List of CSS classes for the table tag.
protected function get_table_classes() {
return [ 'widefat', 'fixed', 'striped', $this->_args['plural'] ];
* Generate the table navigation above or below the table
protected function display_tablenav( $which ) {
if ( 'top' === $which ) {
wp_nonce_field( 'bulk-' . $this->_args['plural'] );
<div class="tablenav <?php echo esc_attr( $which ); ?>">
<?php if ( $this->has_items() ) : ?>
<div class="alignleft actions bulkactions">
<?php $this->bulk_actions( $which ); ?>
$this->extra_tablenav( $which );
$this->pagination( $which );
* Extra controls to be displayed between bulk actions and pagination
protected function extra_tablenav( $which ) {}
* Generate the tbody element for the list table.
public function display_rows_or_placeholder() {
if ( $this->has_items() ) {
echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">';
* Generate the table rows
public function display_rows() {
foreach ( $this->items as $item ) {
$this->single_row( $item );
* Generates content for a single row of the table
* @param object $item The current item
public function single_row( $item ) {
$this->single_row_columns( $item );
* @param string $column_name
protected function column_default( $item, $column_name ) {}
protected function column_cb( $item ) {}
* Generates the columns for a single row of the table
* @param object $item The current item
protected function single_row_columns( $item ) {
list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
foreach ( $columns as $column_name => $column_display_name ) {
$classes = "$column_name column-$column_name";
if ( $primary === $column_name ) {
$classes .= ' has-row-actions column-primary';
if ( in_array( $column_name, $hidden ) ) {
// Comments column uses HTML in the display name with screen reader text.
// Instead of using esc_attr(), we strip tags to get closer to a user-friendly string.
$data = 'data-colname="' . wp_strip_all_tags( $column_display_name ) . '"';
$attributes = "class='$classes' $data";
if ( 'cb' === $column_name ) {
echo '<th scope="row" class="check-column">';
echo $this->column_cb( $item );
} elseif ( method_exists( $this, '_column_' . $column_name ) ) {
[ $this, '_column_' . $column_name ],
} elseif ( method_exists( $this, 'column_' . $column_name ) ) {
echo call_user_func( [ $this, 'column_' . $column_name ], $item );
echo $this->handle_row_actions( $item, $column_name, $primary );
echo $this->column_default( $item, $column_name );
echo $this->handle_row_actions( $item, $column_name, $primary );
* Generates and display row actions links for the list table.
* @param object $item The item being acted upon.
* @param string $column_name Current column name.
* @param string $primary Primary column name.
* @return string The row actions HTML, or an empty string if the current column is the primary column.
protected function handle_row_actions( $item, $column_name, $primary ) {
return $column_name === $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>' : '';
* Handle an incoming ajax request (called from admin-ajax.php)
public function ajax_response() {
if ( ! empty( $_REQUEST['no_placeholder'] ) ) {
$this->display_rows_or_placeholder();
$response = [ 'rows' => $rows ];
if ( isset( $this->_pagination_args['total_items'] ) ) {
$response['total_items_i18n'] = sprintf(
_n( '%s item', '%s items', $this->_pagination_args['total_items'] ),
number_format_i18n( $this->_pagination_args['total_items'] )
if ( isset( $this->_pagination_args['total_pages'] ) ) {
$response['total_pages'] = $this->_pagination_args['total_pages'];
$response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] );
die( PUM_Utils_Array::safe_json_encode( $response ) );
* Send required variables to JavaScript land
public function _js_vars() {
'class' => get_class( $this ),
'id' => $this->screen->id,
'base' => $this->screen->base,
printf( "<script type='text/javascript'>list_args = %s;</script>\n", PUM_Utils_Array::safe_json_encode( $args ) );