6

Limit Comments by Meta in WordPress

Posted (Updated ) in PHP

Update 2015-05-02: Now working for WP 4.2

In a project today we needed to attach a meta value to a comment based on the user commenting, then filter the comments list (both frontend and backend) by this value. The task turned out to be surprisingly difficult and the comment actions/filters are notoriously badly documented in WP so I thought I’d document the process here.

1. Attach a meta value as a visitor comments

A simple comment_post action (not documented) hook does the trick here.

1
2
3
4
5
6
/**
 * Adds a meta value to a comment as one is created
 */
add_action( 'comment_post', function($comment_id) {
	add_comment_meta( $comment_id, 'my_meta_key', 'my_meta_value' );
});

 

2. Filter the comments by meta (4.2+)

Until 4.2 this was done a little differently (See the bottom of this post for that code) however due to recent changes we’re now forced to use comments_clauses action like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
 * Limits displayed comments on front and backend to just those with the
 * specified meta key/value pair.
 *
 * @param array $pieces
 * @param WP_Comment_Query $query
 * @return array
 */
add_action('comments_clauses', function(array $pieces, WP_Comment_Query $query) {
    global $wpdb;
 
    $meta_query = new WP_Meta_Query();
    $meta_query->parse_query_vars([
        'meta_key' => 'my_meta_key',        'meta_value' => 'my_meta_value',    ]);
 
    if ( !empty($meta_query->queries) )
    {
        $meta_query_clauses = $meta_query->get_sql( 'comment', $wpdb->comments, 'comment_ID', $query );
 
        if ( !empty($meta_query_clauses) )
        {
            $pieces['join'] .= $meta_query_clauses['join'];
 
            if ( $pieces['where'] )
                $pieces['where'] .= ' AND ';
            // Strip leading 'AND'.
            $pieces['where'] .= preg_replace( '/^\s*AND\s*/', '', $meta_query_clauses['where'] );
 
            if ( !$query->query_vars['count'] )
            {
                $pieces['groupby'] = "{$wpdb->comments}.comment_ID";
            }
        }
    }
 
    return $pieces;
}, 10, 2);

 

3. Making get_comments_number() work

get_comments_number() method commonly used in your themes comments.php file will return the total number of comments associated with a post. We need it filtered by our above meta query. Here’s an inefficient but effective way to make that happen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * Filter get_comments_number() correctly by our meta query.
 *
 * @param int $count
 * @param int $post_id
 * @return int
 */
add_filter('get_comments_number', function($count, $post_id) {
    $query = new WP_Comment_Query(['post_id' => $post_id]);
 
    // Frontend users only see approved comments
    if ( !is_admin() )
        $comment_query['status'] = 'approve';
 
    return sizeof($query->get_comments());
}, 10, 2);

 

Conclusion

That should be it! If anything doesn’t work as expected let me know in the comments below.

 

Bonus: Filter the comments by meta (<4.2)

Filtering comments by meta in <4.2 is done in two steps. One for admin, and one for frontend.

Admin

This can be done with pre_get_comments action hook (again, not documented – but I did find a usage example here).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * Limit visible comments to just those for the current subdomain.
 * Due to a bug in WP this will work for all backend users but only for 
 * logged out users on the frontend.
 */
add_action('pre_get_comments', function($query) {
	// meta_query is already an instance of WP_Comment_Query but other 
	// query_vars may have already been added, so re-initialize it
	$query->meta_query = new WP_Comment_Query();
 
	$query->query_vars['meta_key'] = 'my_meta_key';
	$query->query_vars['meta_value'] = 'my_meta_value';
 
	$query->meta_query->parse_query_vars( $query->query_vars );
});

Frontend

As the pre_get_comments comment mentions, there is a bug in WP whereby logged in users will not have the above filter applied, so we need to use another hook for those.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * WP doesn't support pre_get_comments for logged in users, so for those
 * we need to manually remove all the comments that don't have  our meta
 * key
 *
 * See https://core.trac.wordpress.org/ticket/27018#ticket
 */
add_filter('comments_array', function($comments, $post_id) {
	global $wpdb, $user_ID;
 
	// If there's no user_ID and no commenter then the pre_get_comments filter
	// applied correctly and we don't need to do anything
	// See comments_template() method
	$commenter = wp_get_current_commenter();
	$comment_author = $commenter['comment_author']; // Escaped by sanitize_comment_cookies()
 
	if ( $user_ID || !empty($comment_author) )
		return;
 
	// Grab comment IDs for this current area
	$area_comment_ids = $wpdb->get_col($wpdb->prepare("
		SELECT $wpdb->comments.comment_ID
		FROM $wpdb->comments
		JOIN $wpdb->commentmeta ON $wpdb->commentmeta.comment_id=$wpdb->comments.comment_ID
		WHERE $wpdb->commentmeta.meta_key=%s AND $wpdb->commentmeta.meta_value=%s
	", 'my_meta_key', 'my_meta_value'));
 
	// Strip out any comments not belonging to the current area
	foreach ( $comments as $key => $comment )
		if ( !in_array($comment->comment_ID, $area_comment_ids) )
			unset($comments[$key]);
 
	return $comments;
}, 10, 2);

The above seems really messy and inefficient to me but I can’t think of a better way. It’ll grab all comments we want to show and filter out any others.