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.