1

I have two custom plugins that use the WP Logging class. It creates a custom post type called Logs, which (at least in my case) is only visible inside the admin, for example at /wp-admin/edit.php?s&post_status=all&post_type=wp_log.

To make it easier to tell which plugin is creating the logs, I've added a few hooks. Both plugins use them identically. What they do is:

  1. Add a sortable Type column to the manage posts screen, and add the type field (which the WP Logging library adds) to that screen.
  2. Add a dropdown to the top of the screen (next to the date filter) and allow the posts to be filtered by which type value they use.

Here's the code I'm using:

// add a sortable Type column to the posts admin
add_filter( 'manage_edit-wp_log_columns', array( $this, 'type_column' ), 10, 1 );
add_filter( 'manage_edit-wp_log_sortable_columns', array( $this, 'sortable_columns' ), 10, 1 );
add_action( 'manage_wp_log_posts_custom_column', array( $this, 'type_column_content' ), 10, 2 );

// filter the log posts admin by log type
add_filter( 'parse_query', array( $this, 'posts_filter' ), 10, 1 );
add_action( 'restrict_manage_posts', array( $this, 'restrict_log_posts' ) );

/**
 * Add a Type column to the posts admin for this post type
 *
 * @param array $columns
 * @return array $columns
 */
public function type_column( $columns ) {
    $columns['type'] = __( 'Type', 'form-processor-mailchimp' );
    return $columns;
}

/**
 * Make the Type column in the posts admin for this post type sortable
 *
 * @param array $columns
 * @return array $columns
 */
public function sortable_columns( $columns ) {
    $columns['type'] = 'type';
    return $columns;
}

/**
 * Add the content for the Type column in the posts admin for this post type
 *
 * @param string $column_name
 * @param int $post_id
 */
public function type_column_content( $column_name, $post_id ) {
    if ( 'type' != $column_name ) {
        return;
    }
    // get wp_log_type
    $terms = wp_get_post_terms(
        $post_id,
        'wp_log_type',
        array(
            'fields' => 'names',
        )
    );
    if ( is_array( $terms ) ) {
        echo esc_attr( $terms[0] );
    }
}

/**
 * Filter log posts by the taxonomy from the dropdown when a value is present
 *
 * @param object $query
 * @return object $query
 */
public function posts_filter( $query ) {
    global $pagenow;
    $type     = 'wp_log';
    $taxonomy = 'wp_log_type';
    if ( is_admin() && 'edit.php' === $pagenow ) {
        if ( isset( $_GET['post_type'] ) && esc_attr( $_GET['post_type'] ) === $type ) {
            if ( isset( $_GET[ $taxonomy ] ) && '' !== $_GET[ $taxonomy ] ) {
                $query->post_type = $type;
                $query->tax_query = array(
                    array(
                        'taxonomy' => $taxonomy,
                        'field'    => 'slug',
                        'terms'    => esc_attr( $_GET[ $taxonomy ] ),
                    ),
                );
            }
        }
    }
}

/**
 * Add a filter form for the log admin so we can filter by wp_log_type taxonomy values
 *
 * @param object $query
 * @return object $query
 */
public function restrict_log_posts() {
    $type     = 'wp_log';
    $taxonomy = 'wp_log_type';
    // only add filter to post type you want
    if ( isset( $_GET['post_type'] ) && esc_attr( $_GET['post_type'] ) === $type ) {
        // get wp_log_type
        $terms = get_terms(
            [
                'taxonomy'   => $taxonomy,
                'hide_empty' => true,
            ]
        );
        ?>
        <select name="wp_log_type">
            <option value=""><?php _e( 'All log types ', 'form-processor-mailchimp' ); ?></option>
            <?php
            $current_log_type = isset( $_GET[ $taxonomy ] ) ? esc_attr( $_GET[ $taxonomy ] ) : '';
            foreach ( $terms as $key => $term ) {
                printf(
                    '<option value="%s"%s>%s</option>',
                    $term->slug,
                    $term->slug == $current_log_type ? ' selected="selected"' : '',
                    $term->name
                );
            }
            ?>
        </select>
        <?php
    }
}

The code is identical between the two plugins in this case. Of course the problem, if it's enabled in both plugins, is that it duplicates the display.

posts manage screen with two identical type dropdowns, and the type displayed twice in the column

Is there any way I can check to see if these additions have already been made before making them?

I thought this was worth a try (on all the filters, and using doing_action on all the actions):

if ( ! doing_filter( 'manage_edit-wp_log_columns' ) ) {
    add_filter( 'manage_edit-wp_log_columns', array( $this, 'type_column' ), 10, 1 );
}

But it didn't change anything. Is there anything else I can try on this?

  • Have you considered not loading the functionality if it's already defined, then having a taxonomy that identifies the source of the log file? Loading 2 implementations of the same thing should have caused PHP fatal errors, to rename the functions so they still load just gives you new problems – Tom J Nowell Aug 28 at 15:48
  • @TomJNowell I'm not sure I understand the question. The logging library, in this case, is used to log different things by the two plugins. So the library itself is only loaded once, but then both plugins create a different type of log (it's a taxonomy), so it is helpful (at least for me) to be able to filter it. Currently only one of the plugins is used by more users than just me, but that's not necessarily always going to be the case. – Jonathan Stegall Aug 28 at 17:30
  • when you go to load/define the logging library, check if it's already loaded, e.g. something like if ( class_exists( 'logging library' ) ) { return; }. If it's already loaded, then don't load it again. The same goes for your code that adds the filter for the roles. You don't need to load both copies – Tom J Nowell Aug 28 at 21:34
  • @TomJNowell this would work, but the way the logging library works is that the calling plugin has to extend it. So the hooks I'm calling aren't part of the logging library. If I run a check to see if that class is loaded, the additions and settings are never run. It could be a less than ideal library in that case, but that's how it works. – Jonathan Stegall Aug 29 at 16:01
1

In your code, you add the filter like this:

add_action( 'restrict_manage_posts', array( $this, 'restrict_log_posts' ) );

Which adds a function that starts like this:

public function restrict_log_posts() {
    $type     = 'wp_log';
    $taxonomy = 'wp_log_type';
    // only add filter to post type you want
    if ( isset( $_GET['post_type'] ) && esc_attr( $_GET['post_type'] ) === $type ) {
        // get wp_log_type
        $terms = get_terms(

First, this action passes the post type so it can be simplified by adjusting the add_action and using the first parameter it passes:

public function restrict_log_posts( $post_type ) {
    $type     = 'wp_log';
    $taxonomy = 'wp_log_type';
    // only add filter to post type you want
    if ( 'wp_log' === $post_type ) {
        // get wp_log_type
        $terms = get_terms(

Then we could use did_action to only run it the first time:

public function restrict_log_posts( $post_type ) {
    if ( did_action( 'restrict_manage_posts' ) ) {
        return;
    }
....

But this might not have the desired effect, and it's crude. Instead, this action doesn't belong in this class at all, and should be a standalone action. Then we can define the function only if it isn't already defined:


if ( !function_exists('restrict_logs_by_type') ) {
    add_action( 'restrict_manage_posts', 'restrict_logs_by_type', 10, 1 );
    /**
     * Add a filter form for the log admin so we can filter by wp_log_type taxonomy values
     *
     */
    function restrict_logs_by_type( $post_type ) {
        $type     = 'wp_log';
        $taxonomy = 'wp_log_type';
        // only add filter to post type you want
        if ( $type !== $post_type ) {
            return;
        }
        // get wp_log_type
        $terms = get_terms([
                'taxonomy'   => $taxonomy,
                'hide_empty' => true,
        ]);
        if ( is_wp_error( $terms ) || empty( $terms ) ) {
            // no terms, or the taxonomy doesn't exist, skip
            return;
        }

        ?>
        <select name="wp_log_type">
            <option value=""><?php esc_html_e( 'All log types ', 'form-processor-mailchimp' ); ?></option>
            <?php
            $current_log_type = isset( $_GET[ $taxonomy ] ) ? esc_attr( $_GET[ $taxonomy ] ) : '';
            foreach ( $terms as $key => $term ) {
                printf(
                    '<option value="%s"%s>%s</option>',
                    esc_attr( $term->slug ),
                    selected( $term->slug, $current_log_type, false ),
                    esc_html( $term->name )
                );
            }
            ?>
        </select>
        <?php
    }
}

Note that I made a number of significant improvements:

  • I fixed a major security hole by adding escaping to your option tags
  • I replaced the ugly ternary operator with the selected function provided by WP
  • I replaced the if check with a !==, making it a guard. Now the entire function can be unindented by 1, and is more readable. Additionally both the Cyclomatic and NPath complexity have been reduced
  • The function is now standalone, there was never any need for it to be part of an object or class
  • I renamed the function to describe what it does
  • If no terms are found, or the taxonomy doesn't exist, this version skips displaying the dropdown entirely. No sense having a dropdown if the only item is "All". Without this, the log screen would crash if there were no log types or entries
  • The entire snippet is wrapped in a function_exists call, if the function is loaded by the first plugin, the second plugin won't bother defining and adding it
  • When you say "there was never any need for it to be part of an object or class," I guess I don't disagree, but do you have any thoughts about what to do when the whole plugin is already composed of various class files? Would you say that that's a bad plugin structure entirely? – Jonathan Stegall Aug 29 at 17:32
  • You don't have to make the entire codebase class based, or not class based. Use what's appropriate for the task at hand – Tom J Nowell Aug 29 at 18:24
0

Now that I've been looking at it, I thought of a possible solution.

// add a filter to check for other plugins that might be filtering the log screen
$are_logs_filtered = apply_filters( 'wp_logging_manage_logs_filtered', false );
add_filter( 'wp_logging_manage_logs_filtered', '__return_true' );

if ( false === $are_logs_filtered ) {
    // add a sortable Type column to the posts admin
    add_filter( 'manage_edit-wp_log_columns', array( $this, 'type_column' ), 10, 1 );
    add_filter( 'manage_edit-wp_log_sortable_columns', array( $this, 'sortable_columns' ), 10, 1 );
    add_action( 'manage_wp_log_posts_custom_column', array( $this, 'type_column_content' ), 10, 2 );

    // filter the log posts admin by log type
    add_filter( 'parse_query', array( $this, 'posts_filter' ), 10, 1 );
    add_action( 'restrict_manage_posts', array( $this, 'restrict_log_posts' ) );
}

Adding a wp_logging_manage_logs_filtered to both plugins, and setting its value to true on both plugins, results in the filtering field, the sorted column, and the content in the column, all only happening once.

I am inclined to think that's the best thing to do, but of course I could be missing something.

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.