Adding Columns to Posts in Admin (Sortable)

Let's talk about creating new columns in the posts table in the admin area. We will also look at how to make such columns sortable, just like the date column. This way, we can create a column with data from a meta-field and then sort the posts based on that data. It's all quite simple.

In this article, as an example, we will create a column "Visits" with data from the meta-field views, where visits are recorded. The column will be sortable.

Hooks for Creating Columns

manage_(screen_id)_columns

Allows adding columns to the posts table on the specified screen (in our case edit-post).

Passes an array with column data that we can modify by adding our column (views) or removing an existing one using unset(). The name of our filter will be: manage_edit-post_columns.

manage_(post_type)_posts_columns
Similar to the previous one - adds columns. Here, the post type is specified, not the screen ID... The name of our filter will be: manage_post_posts_columns. Since version 3.1, it is recommended to use this hook.
manage_(post_type)_posts_custom_column

Responsible for filling the column data on the posts page. In our case: manage_post_posts_custom_column.

Passes the column name and post ID.

manage_(screen_id)_sortable_columns

Similar to the first one — registers a sortable column where we specify the orderby query name.

Also passes an array with registered sortable columns. In our case, the filter looks like this: manage_edit-post_sortable_columns.

pre_get_posts (wp-includes/query.php)

This filter-action triggers at the very beginning of the get_posts() method of the WP_query class.

The hook passes the entire class by reference (&$this). Using this filter, we can set the parameters of the main WP query ($wp_query), based on which the output is then built.

To find out the screen_id, we use the function get_current_screen(). It can be hooked to in_admin_header:

add_action( 'in_admin_header', function(){
	echo '<pre>'. print_r( get_current_screen(), 1 ) .'</pre>';
} );

In our case, screen_id = edit-post - this is the post editing page in the admin area.

Creating the Column

Insert the following code into the theme file function.php:

// create a new column
add_filter( 'manage_' . 'post' . '_posts_columns', 'add_views_column', 4 );
function add_views_column( $columns ) {
	$num = 2; // after which column to insert new ones

	$new_columns = [
		'views' => 'Visits',
	];

	return array_slice( $columns, 0, $num ) + $new_columns + array_slice( $columns, $num );
}

// fill the column with data
// wp-admin/includes/class-wp-posts-list-table.php
add_action( 'manage_' . 'post' . '_posts_custom_column', 'fill_views_column', 5, 2 );
function fill_views_column( $colname, $post_id ) {
	if( $colname === 'views' ){
		echo get_post_meta( $post_id, 'views', 1 );
	}
}

At this point, we can stop if we do not need to sort the column — it will simply display the data.

Making the Column Sortable

// add the ability to sort the column
add_filter( 'manage_edit-' . 'post' . '_sortable_columns', 'add_views_sortable_column' );
function add_views_sortable_column( $sortable_columns ) {
	$sortable_columns['views'] = [ 'views_views', false ];
	// false = asc (default)
	// true  = desc

	return $sortable_columns;
}

Here the key views must match the key when registering the column: $out['views'] and $sortable_columns['views']. The value: views_views will be the value of the orderby query parameter that WordPress will automatically add (&orderby=views_views). This same value will be added to the WP_query request parameters and if it matches known WP values ('title', 'date', 'modified', 'comment_count', etc.), WP will sort the column as needed and we can stop here. Full list of known WP values, exceptions are: meta_value and meta_value_num.

If we specify meta_value instead of views_views, WP will not be able to automatically perform the correct sorting. Therefore, if the orderby parameter specifies our value views_views (&orderby=views_views), we will create a custom query as needed.

To sort by a meta-field, it is easiest to change the arguments of the base query using the pre_get_posts hook. But it is important to understand that this hook is global and triggers every time a page is generated, not only in the admin area but also on the front end. Therefore, we must specify precisely when to modify the query, in our case, this will be when the orderby argument equals views_views. In all other cases, we do not touch the query.

Option 1:

// modify the query when sorting the column
add_action( 'pre_get_posts', 'add_column_views_request' );
function add_column_views_request( $query ) {
	if( ! is_admin()
		|| ! $query->is_main_query()
		|| $query->get( 'orderby' ) !== 'views_views'
		|| get_current_screen()->id !== 'edit-post'
	){
		return;
	}

	$query->set( 'meta_key', 'views' );
	$query->set( 'orderby', 'meta_value_num' );
}

Option 2: the principle is exactly the same, just using the request hook:

// modify the query when sorting the column
add_filter( 'request', 'add_column_views_request' );
function add_column_views_request( $vars ) {
	if( isset( $vars['orderby'] ) && $vars['orderby'] === 'views_views' ){
		$vars['meta_key'] = 'views';
		$vars['orderby'] = 'meta_value_num';
	}

	return $vars;
}

Option 3: the principle is taken from the article Sortable Taxonomy Columns.

Here we modify the SQL query, not the parameters passed to WP_query. This will be useful if we need to create some unique sorting. I adapted it to our case:

// modify the query when sorting the column
add_filter( 'posts_clauses', 'add_column_views_request', 10, 2 );
function add_column_views_request( $clauses, $wp_query ) {
	if( 'views_views' != $wp_query->query['orderby'] ){
		return $clauses;
	}

	global $wpdb;

	$clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} ON {$wpdb->posts}.ID={$wpdb->postmeta}.post_id";
	//$clauses['where'] .= " AND {$wpdb->postmeta}.meta_key='views'";
	$clauses['orderby'] = " {$wpdb->postmeta}.meta_value+0 ";
	$clauses['orderby'] .= ( 'ASC' == strtoupper( $wp_query->get( 'order' ) ) ) ? 'ASC' : 'DESC';

	// other modifiable elements
	//$clauses['groupby']
	//$clauses['distinct']
	//$clauses['fields'] // wp_posts.*
	//$clauses['limits'] // LIMIT 0, 20

	return $clauses;
}

Posts with empty meta-fields (it will not exist for the post) will not be included in the selection.

Note

The above options show that when sorting, only those records with the specified meta-field exist. It is complicated to sort so that there are also records where the field does not exist! For this, a separate complex query would need to be written. It is much simpler and more logical to stick with what we get...

For example, doing it like this (or in a similar way) will not work:

// NON-WORKING CODE!!!
add_action( 'pre_get_posts', 'pre_get_posts_views' );
function pre_get_posts_views( $wp_query ) {

		$wp_query->set( 'orderby', 'views_exists' );

		$wp_query->set( 'meta_query', [
			'relation'         => 'OR',
			'views_exists'     => [
				'key'     => 'views',
				'compare' => 'EXISTS',
				'type'    => 'numeric',
			],
			[
				'key'     => 'views',
				'compare' => 'NOT EXISTS',
			},
		] );
	}
}

Column Width

It may also be useful to edit the column width, as it sometimes stretches unnecessarily. We specify the width like this:

// adjust the column width via css
add_action('admin_head', 'add_views_column_css');
function add_views_column_css(){
	echo '<style type="text/css">.column-views{ width:10%; }</style>';
}

Full Code (as a Class)

/**
 * Additional sortable columns for posts in the admin area
 */
final class My_Sortable_Post_Columns {

	public static function init() {
		// create a new column
		add_filter( 'manage_post_posts_columns', [ __CLASS__, 'add_columns' ], 4 );
		// fill the column with data -  wp-admin/includes/class-wp-posts-list-table.php
		add_filter( 'manage_post_posts_custom_column', [ __CLASS__, 'fill_columns' ], 5, 2 );
		// adjust the column width via css
		add_action( 'admin_head', [ __CLASS__, '_css' ] );
		// add the ability to sort the column
		add_filter( 'manage_edit-post_sortable_columns', [ __CLASS__, 'add_sortable_columns' ] );
		// modify the query when sorting the column
		add_filter( 'pre_get_posts', [ __CLASS__, 'handle_sort_request' ] );
	}

	public static function add_columns( $columns ) {

		// insert in the right place - 3 - 3rd column
		$out = [];
		foreach( $columns as $col => $name ){
			if( ++$i == 3 ){
				$out['views'] = 'Visits';
			}
			$out[ $col ] = $name;
		}

		return $out;
	}

	public static function add_sortable_columns( $sortable_columns ) {
		$sortable_columns['views'] = 'views_views';

		return $sortable_columns;
	}

	public static function fill_columns( $colname, $post_id ) {
		if( $colname === 'views' ){
			echo get_post_meta( $post_id, 'views', 1 );
		}
	}

	public static function _css() {
		if( 'edit' === get_current_screen()->base ){
			echo '<style>.column-views{ width:10%; }</style>';
		}
	}

	public static function handle_sort_request( $object ) {
		if( $object->get( 'orderby' ) != 'views_views' ){
			return;
		}

		$object->set( 'meta_key', 'views' );
		$object->set( 'orderby', 'meta_value_num' );
	}
}