Add “Last Edited by” column to custom post type list table

Changing admin columns belongs to a plugin, not to a theme file, because themes should never change anything else than frontend output. You can get the complete plugin here: Plugin Product Editor Column.

Looking at the docs you linked to, I see the plugin author requires a child class that mixes multiple separate tasks. That’s not good. We will see in a minute why.

On the other hand, we are not in the plugin support business anyway. We are in the Find your own solution business. So let’s ignore that plugin and look at the core functions.

enter image description here

  • Column headers for post types are registered on the filter "manage_{$post_type}_posts_columns". For a post type product this would be "manage_product_posts_columns".
  • The column content can be printed on the action "manage_{$post_type}_posts_custom_column".

These are very poor names: They don’t really tell us what happens here.

Both hooks are called when the file wp-admin/edit.php is loaded, so we wait until the action 'load-edit.php' fires.

On the edit page, we look into the object returned by get_current_screen(): If its id property matches "edit-$post_type", we register our callbacks.

The "manage_{$post_type}_posts_columns" filter gives us an array of the existing column headers. We just add an entry and return the array back:

function add_column( Array $columns )
{
    $columns[ 'modified_author' ] = 'Last modified by';

    return $columns;
}

The "manage_{$post_type}_posts_custom_column" action gives us the $column_name and the $post_id. We compare the column name with our earlier registered name 'modified_author', because there might be other custom columns, and we don’t want to touch them. If the given column name is ours, we use the $post_id to get the ID of the last author who modified the post:

$last_id   = get_post_meta( $post_id, '_edit_last', TRUE );
$last_user = get_userdata( $last_id );
print esc_html( $last_user->display_name );

The short version of our code could look like this:

add_action( 'load-edit.php', function() {

    $post_type="product";
    $col_name="modified_author";

    $screen = get_current_screen();

    if ( ! isset ( $screen->id ) )
        return;

    if ( "edit-$post_type" !== $screen->id )
        return;

    add_filter(
        "manage_{$post_type}_posts_columns",
        function( $posts_columns ) use ( $col_name ) {
            $posts_columns[ $col_name ] = 'Last modified by';

            return $posts_columns;
        }
    );

    add_action(
        "manage_{$post_type}_posts_custom_column",
        function( $column_name, $post_id ) use ( $col_name ) {

            if ( $col_name !== $column_name )
                return;

            $last_id = get_post_meta( $post_id, '_edit_last', TRUE );

            if ( ! $last_id ) {
                print '<i>Unknown</i>';
                return;
            }

            $last_user = get_userdata( $last_id );

            print esc_html( $last_user->display_name );
        },
        10, 2
    );
});

This works, but it isn’t good.

  • We cannot reuse that code to add the same column to posts or pages or something else.
  • We cannot test the code, because everything happens in one call.
  • Separation of concerns doesn’t happen. We are mixing too many different tasks:
    • Registering the callbacks
    • Validating the column name
    • Fetching the value from the post meta table
    • Printing the escaped output of the column

Let’s improve our code!

Registering the callbacks should be done by a Controller. It doesn’t have to know where we get our data from and how we print them to the user’s screen, so pass those as abstract dependencies:

class Controller
{
    /**
     * @var Column_Data
     */
    private $data;

    /**
     * @var Column_View
     */
    private $view;

    /**
     * @param Column_Data $data
     * @param Column_View $view
     */
    public function __construct( Column_Data $data, Column_View $view ) {

        $this->data = $data;
        $this->view = $view;
    }

    /**
     * @return void
     */
    public function setup()
    {
        $screen    = get_current_screen();
        $post_type = $this->data->get_post_type();

        if ( ! isset ( $screen->id ) )
            return;

        if ( "edit-$post_type" !== $screen->id )
            return;

        add_filter(
            "manage_{$post_type}_posts_columns",
            [ $this->data, 'add_column' ]
        );

        add_action(
            "manage_{$post_type}_posts_custom_column",
            [ $this->view, 'render_column' ],
            10, 2
        );
    }
}

Column_Data and Column_View are interfaces, not concrete classes, so we can reuse that controller with different data providers or output handlers. Let’s build these interfaces.

The interface for the data needs methods to

  • add the column headers
  • get the content (author display name for example)
  • tell the controller what post type it handles and
  • the view if it operates in the correct column when it is called
interface Column_Data
{
    /**
     * @param array $columns
     * @return array
     */
    public function add_column( Array $columns );

    /**
     * @param  int    $post_id
     * @return string
     */
    public function get_column_content( $post_id );

    /**
     * @return string
     */
    public function get_post_type();

    /**
     * @param $column_name
     * @return bool
     */
    public function is_valid_column( $column_name );
}

The output/view object needs just one method:

interface Column_View
{
    /**
     * @param  string $column_name
     * @param  int    $post_id
     * @return void
     */
    public function render_column( $column_name, $post_id );
}

The view has to fetch some information from the data model, so pass an instance to the constructor of the concrete class:

class Last_Mod_Author_Column_Output implements Column_View
{
    /**
     * @var Column_Data
     */
    private $data;

    /**
     * @param Column_Data $data
     */
    public function __construct( Column_Data $data )
    {
        $this->data = $data;
    }

    /**
     * @param  string $column_name
     * @param  int    $post_id
     * @return void
     */
    public function render_column( $column_name, $post_id )
    {
        if ( ! $this->data->is_valid_column( $column_name ) )
            return;

        $content = $this->data->get_column_content( $post_id );

        if ( '' === $content )
            print '<i>Unknown</i>';
        else
            print esc_html( $content );
    }
}

The data model has to know the post type, so we pass that to the concrete class’ constructor:

class Last_Mod_Author_Column_Data implements Column_Data
{
    /**
     * @var string
     */
    private $post_type;

    /**
     * @var string
     */
    private $column_name="modified_author";

    /**
     * @param string $post_type
     */
    public function __construct( $post_type )
    {
        $this->post_type = $post_type;
    }

    /**
     * @param array $columns
     * @return array
     */
    public function add_column( Array $columns )
    {
        $columns[ $this->column_name ] = 'Last modified by';

        return $columns;
    }

    /**
     * @param  int    $post_id
     * @return string
     */
    public function get_column_content( $post_id )
    {
        $last_id = get_post_meta( $post_id, '_edit_last', TRUE );

        if ( ! $last_id ) {
            return '';
        }

        $last_user = get_userdata( $last_id );

        return $last_user->display_name;
    }

    /**
     * @return string
     */
    public function get_post_type()
    {
        return $this->post_type;
    }

    /**
     * @param $column_name
     * @return bool
     */
    public function is_valid_column( $column_name )
    {
        return $this->column_name === $column_name;
    }
}

And now we can use these classes:

add_action( 'load-edit.php', function() {

    $model      = new Last_Mod_Author_Column_Data( 'product' );
    $view       = new Last_Mod_Author_Column_Output( $model );
    $controller = new Controller( $model, $view );

    $controller->setup();
});

We can use the same code for pages with just a tiny change:

add_action( 'load-edit.php', function() {

    $model      = new Last_Mod_Author_Column_Data( 'page' );
    $view       = new Last_Mod_Author_Column_Output( $model );
    $controller = new Controller( $model, $view );

    $controller->setup();
});

Why is this rather long code better?

We could implement the data interface in a class that fetches the value from another table or a text file or an external API – without any change to the controller or the view.

We could use a view that makes the author name bold or uses a different fallback – without changing the controller or the data model.

We can test all public methods of all classes by providing dummy objects for the dependencies (stubs).

We avoid the fragile base class problem, because we don’t use inheritance like the author of the plugin you mentioned.

Leave a Comment